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)
118 class RarslaveRepairer (object):
119 # Verify (and repair) the set
120 # Make sure it worked, otherwise clean up and return failure
122 def __init__ (self, dir, file, join=False):
123 self.dir = dir # the directory containing the par2 file
124 self.file = file # the par2 file
125 self.join = join # True if the par2 set is 001 002 ...
127 assert os.path.isdir (dir)
128 assert os.path.isfile (os.path.join (dir, file))
130 def checkAndRepair (self):
132 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
133 PAR2_CMD = config.get_value ('commands', 'par2repair')
136 basename = get_basename (self.file)
137 all_files = find_likely_files (self.dir, self.file)
139 par2_files = find_par2_files (all_files)
141 # assemble the command
142 command = "%s \"%s\" " % (PAR2_CMD, self.file)
146 command += "\"%s\" " % os.path.split (f)[1]
150 if f not in par2_files:
151 command += "\"%s\" " % os.path.split (f)[1]
154 ret = run_command (command, self.dir)
158 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.file, RarslaveLogger.MessageType.Fatal)
163 def run_command (cmd, indir=None):
164 # Runs the specified command-line in the directory given (or, in the current directory
165 # if none is given). It returns the status code given by the application.
170 assert os.path.isdir (indir) # MUST be a directory!
173 ret = os.system (cmd)
177 def full_abspath (p):
178 return os.path.abspath (os.path.expanduser (p))
180 def get_basename (name):
181 """Strips most kinds of endings from a filename"""
183 regex = config.get_value ('regular expressions', 'basename_regex')
184 r = re.compile (regex, re.IGNORECASE)
191 g = r.match (name).groups()
197 def find_likely_files (dir, p2file):
198 """Finds files which are likely to be part of the set corresponding
199 to $name in the directory $dir"""
201 assert os.path.isdir (dir)
202 assert os.path.isfile (os.path.join (dir, p2file))
204 basename = get_basename (p2file)
206 dir = os.path.abspath (dir)
207 ename = re.escape (basename)
208 regex = re.compile ('^%s.*$' % (ename, ))
210 name_matches = [f for f in os.listdir (dir) if regex.match (f)]
212 parsed_matches = Par2Parser.get_protected_files (dir, p2file)
213 except EnvironmentError:
215 logger.addMessage ('Bad par2 file: %s' % p2file, RarslaveLogger.MessageType.Fatal)
217 return name_matches + parsed_matches
219 def find_par2_files (files):
220 """Find all par2 files in the list $files"""
222 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
223 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
224 return [f for f in files if regex.match (f)]
226 def find_all_par2_files (dir):
227 """Finds all par2 files in a directory"""
228 # NOTE: does NOT return absolute paths
230 if not os.path.isdir (os.path.abspath (dir)):
231 raise ValueError # bad directory given
233 dir = os.path.abspath (dir)
234 files = os.listdir (dir)
236 return find_par2_files (files)
238 def find_extraction_heads (dir, files):
239 """Takes a list of possible files and finds likely heads of
242 # NOTE: perhaps this should happen AFTER repair is
243 # NOTE: successful. That way all files would already exist
245 # According to various sources online:
246 # 1) pre rar-3.0: .rar .r00 .r01 ...
247 # 2) post rar-3.0: .part01.rar .part02.rar
248 # 3) zip all ver: .zip
251 p2files = find_par2_files (files)
253 # Old RAR type, find all files ending in .rar
254 if is_oldrar (files):
255 extractor = RarslaveExtractor (TYPE_OLDRAR)
256 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
259 extractor.addHead (dir, f)
261 if is_newrar (files):
262 extractor = RarslaveExtractor (TYPE_NEWRAR)
263 regex = re.compile ('^.*\.part0*1.rar$', re.IGNORECASE)
266 extractor.addHead (dir, f)
269 extractor = RarslaveExtractor (TYPE_ZIP)
270 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
273 extractor.addHead (dir, f)
275 if is_noextract (files):
276 # Use the Par2 Parser (from cfv) here to find out what files are protected.
277 # Since these are not being extracted, they will be mv'd to another directory
279 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
284 prot_files = Par2Parser.get_protected_files (dir, f)
286 except EnvironmentError:
287 logger.addMessage ('Error parsing PAR2 file: %s', f)
295 extractor.addHead (dir, f)
297 logger.addMessage ('Error parsing all PAR2 files in this set ...')
299 # Make sure we found the type
300 if extractor == None:
301 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0],
302 RarslaveLogger.MessageType.Verbose)
304 # No-heads here, but it's better than failing completely
305 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
309 def generic_matcher (files, regex, nocase=False):
310 """Run the regex over the files, and see if one matches or not.
311 NOTE: this does not return the matches, just if a match occurred."""
314 cregex = re.compile (regex, re.IGNORECASE)
316 cregex = re.compile (regex)
324 def is_oldrar (files):
325 return generic_matcher (files, '^.*\.r00$')
327 def is_newrar (files):
328 return generic_matcher (files, '^.*\.part0*1\.rar$')
331 return generic_matcher (files, '^.*\.zip$')
333 def is_noextract (files):
334 # Type that needs no extraction.
335 # TODO: Add others ???
336 return generic_matcher (files, '^.*\.001$')
338 def find_deleteable_files (files):
339 # Deleteable types regex should come from the config
341 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
342 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
344 return [f for f in files if dregex.match (f)]
350 class PAR2Set (object):
356 def __init__ (self, dir, file):
357 assert os.path.isdir (dir)
358 assert os.path.isfile (os.path.join (dir, file))
363 basename = get_basename (file)
364 self.likely_files = find_likely_files (dir, file)
366 def __list_eq (self, l1, l2):
368 if len(l1) != len(l2):
377 def __eq__ (self, rhs):
378 return self.__list_eq (self.likely_files, rhs.likely_files)
381 par2files = find_par2_files (self.likely_files)
382 par2head = par2files[0]
384 join = is_noextract (self.likely_files)
387 repairer = RarslaveRepairer (self.dir, par2head, join)
388 ret = repairer.checkAndRepair ()
391 logger.addMessage ('Repair stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
395 EXTRACT_DIR = options.extract_dir
396 extractor = find_extraction_heads (self.dir, self.likely_files)
397 ret = extractor.extract (EXTRACT_DIR)
400 logger.addMessage ('Extraction stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
404 DELETE_INTERACTIVE = options.interactive
405 deleteable_files = find_deleteable_files (self.likely_files)
406 ret = delete_list (self.dir, deleteable_files, DELETE_INTERACTIVE)
409 logger.addMessage ('Deletion stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
412 logger.addMessage ('Successfully completed: %s' % par2head)
415 def delete_list (dir, files, interactive=False):
416 # Delete a list of files
418 assert os.path.isdir (dir)
421 valid_y = ['Y', 'YES']
422 valid_n = ['N', 'NO', '']
426 print 'Do you want to delete the following?:'
428 s = raw_input ('Delete [y/N]: ').upper()
430 if s in valid_y + valid_n:
437 os.remove (os.path.join (dir, f))
442 def generate_all_parsets (dir):
443 # Generate all parsets in the given directory.
445 assert os.path.isdir (dir) # Directory MUST be valid
448 p2files = find_all_par2_files (dir)
457 def check_required_progs():
458 """Check if the required programs are installed"""
460 shell_not_found = 32512
463 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
464 needed.append ('par2repair')
466 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
467 needed.append ('unrar')
469 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
470 needed.append ('unzip')
474 print 'Needed program "%s" not found in $PATH' % (n, )
478 def run_options (options):
481 options.work_dir = full_abspath (options.work_dir)
483 # Make sure that the directory is valid
484 if not os.path.isdir (options.work_dir):
485 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
486 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
487 sys.stderr.write ('configuration file to override the working directory permanently.\n')
490 if options.extract_dir != None:
491 options.extract_dir = full_abspath (options.extract_dir)
494 print PROGRAM + ' - ' + VERSION
496 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
498 print 'This program comes with ABSOLUTELY NO WARRANTY.'
499 print 'This is free software, and you are welcome to redistribute it'
500 print 'under certain conditions. See the file COPYING for details.'
503 if options.check_progs:
504 check_required_progs ()
506 if options.write_def_config:
507 config.write_config (default=True)
509 if options.write_config:
510 config.write_config ()
512 def find_loglevel (options):
514 loglevel = options.verbose - options.quiet
516 if loglevel < RarslaveLogger.MessageType.Fatal:
517 loglevel = RarslaveLogger.MessageType.Fatal
519 if loglevel > RarslaveLogger.MessageType.Debug:
520 loglevel = RarslaveLogger.MessageType.Debug
524 def printMessageTable (loglevel):
526 if logger.hasFatalMessages ():
527 print '\nFatal Messages\n' + '=' * 80
528 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
530 if loglevel == RarslaveLogger.MessageType.Fatal:
533 if logger.hasNormalMessages ():
534 print '\nNormal Messages\n' + '=' * 80
535 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
537 if loglevel == RarslaveLogger.MessageType.Normal:
540 if logger.hasVerboseMessages ():
541 print '\nVerbose Messages\n' + '=' * 80
542 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
544 if loglevel == RarslaveLogger.MessageType.Verbose:
547 if logger.hasDebugMessages ():
548 print '\nDebug Messages\n' + '=' * 80
549 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
555 # Build the OptionParser
556 parser = optparse.OptionParser()
557 parser.add_option('-n', '--not-recursive',
558 action='store_false', dest='recursive',
559 default=config.get_value('options', 'recursive'),
560 help="Don't run recursively")
562 parser.add_option('-d', '--work-dir',
563 dest='work_dir', type='string',
564 default=config.get_value('directories', 'working_directory'),
565 help="Start running at DIR", metavar='DIR')
567 parser.add_option('-e', '--extract-dir',
568 dest='extract_dir', type='string',
569 default=config.get_value('directories', 'extract_directory'),
570 help="Extract to DIR", metavar='DIR')
572 parser.add_option('-p', '--check-required-programs',
573 action='store_true', dest='check_progs',
575 help="Check for required programs")
577 parser.add_option('-f', '--write-default-config',
578 action='store_true', dest='write_def_config',
579 default=False, help="Write out a new default config")
581 parser.add_option('-c', '--write-new-config',
582 action='store_true', dest='write_config',
583 default=False, help="Write out the current config")
585 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
586 default=config.get_value('options', 'interactive'),
587 help="Confirm before removing files")
589 parser.add_option('-q', '--quiet', dest='quiet', action='count',
590 default=0, help="Output fatal messages only")
592 parser.add_option('-v', '--verbose', dest='verbose', action='count',
593 default=0, help="Output extra information")
595 parser.add_option('-V', '--version', dest='version', action='store_true',
596 default=False, help="Output version information")
598 parser.version = VERSION
600 # Parse the given options
602 (options, args) = parser.parse_args()
604 # Run any special actions that are needed on these options
605 run_options (options)
607 # Find the loglevel using the options given
608 loglevel = find_loglevel (options)
611 if options.recursive:
612 for (dir, subdirs, files) in os.walk (options.work_dir):
613 parsets = generate_all_parsets (dir)
619 parsets = generate_all_parsets (options.work_dir)
624 printMessageTable (loglevel)
629 if __name__ == '__main__':