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)]
211 parsed_matches = par2parser.get_protected_files (dir, p2file)
213 return name_matches + parsed_matches
215 def find_par2_files (files):
216 """Find all par2 files in the list $files"""
218 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
219 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
220 return [f for f in files if regex.match (f)]
222 def find_all_par2_files (dir):
223 """Finds all par2 files in a directory"""
224 # NOTE: does NOT return absolute paths
226 if not os.path.isdir (os.path.abspath (dir)):
227 raise ValueError # bad directory given
229 dir = os.path.abspath (dir)
230 files = os.listdir (dir)
232 return find_par2_files (files)
234 def find_extraction_heads (dir, files):
235 """Takes a list of possible files and finds likely heads of
238 # NOTE: perhaps this should happen AFTER repair is
239 # NOTE: successful. That way all files would already exist
241 # According to various sources online:
242 # 1) pre rar-3.0: .rar .r00 .r01 ...
243 # 2) post rar-3.0: .part01.rar .part02.rar
244 # 3) zip all ver: .zip
247 p2files = find_par2_files (files)
249 # Old RAR type, find all files ending in .rar
250 if is_oldrar (files):
251 extractor = RarslaveExtractor (TYPE_OLDRAR)
252 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
255 extractor.addHead (dir, f)
257 if is_newrar (files):
258 extractor = RarslaveExtractor (TYPE_NEWRAR)
259 regex = re.compile ('^.*\.part0*1.rar$', re.IGNORECASE)
262 extractor.addHead (dir, f)
265 extractor = RarslaveExtractor (TYPE_ZIP)
266 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
269 extractor.addHead (dir, f)
271 if is_noextract (files):
272 # Use the Par2 Parser (from cfv) here to find out what files are protected.
273 # Since these are not being extracted, they will be mv'd to another directory
275 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
280 prot_files = par2parser.get_protected_files (dir, f)
282 except: #FIXME: add the actual exceptions
283 logger.addMessage ('Error parsing PAR2 file: %s', f)
291 extractor.addHead (dir, f)
293 logger.addMessage ('Error parsing all PAR2 files in this set ...')
295 # Make sure we found the type
296 if extractor == None:
297 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0],
298 RarslaveLogger.MessageType.Fatal)
300 # No-heads here, but it's better than failing completely
301 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
305 def generic_matcher (files, regex, nocase=False):
306 """Run the regex over the files, and see if one matches or not.
307 NOTE: this does not return the matches, just if a match occurred."""
310 cregex = re.compile (regex, re.IGNORECASE)
312 cregex = re.compile (regex)
320 def is_oldrar (files):
321 return generic_matcher (files, '^.*\.r00$')
323 def is_newrar (files):
324 return generic_matcher (files, '^.*\.part0*1\.rar$')
327 return generic_matcher (files, '^.*\.zip$')
329 def is_noextract (files):
330 # Type that needs no extraction.
331 # TODO: Add others ???
332 return generic_matcher (files, '^.*\.001$')
334 def find_deleteable_files (files):
335 # Deleteable types regex should come from the config
337 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
338 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
340 return [f for f in files if dregex.match (f)]
346 class PAR2Set (object):
352 def __init__ (self, dir, file):
353 assert os.path.isdir (dir)
354 assert os.path.isfile (os.path.join (dir, file))
359 basename = get_basename (file)
360 self.likely_files = find_likely_files (dir, file)
362 def __list_eq (self, l1, l2):
364 if len(l1) != len(l2):
373 def __eq__ (self, rhs):
374 return self.__list_eq (self.likely_files, rhs.likely_files)
377 par2files = find_par2_files (self.likely_files)
378 par2head = par2files[0]
380 join = is_noextract (self.likely_files)
383 repairer = RarslaveRepairer (self.dir, par2head, join)
384 ret = repairer.checkAndRepair ()
387 logger.addMessage ('Repair stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
391 EXTRACT_DIR = options.extract_dir
392 extractor = find_extraction_heads (self.dir, self.likely_files)
393 ret = extractor.extract (EXTRACT_DIR)
396 logger.addMessage ('Extraction stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
400 DELETE_INTERACTIVE = options.interactive
401 deleteable_files = find_deleteable_files (self.likely_files)
402 ret = delete_list (self.dir, deleteable_files, DELETE_INTERACTIVE)
405 logger.addMessage ('Deletion stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
408 logger.addMessage ('Successfully completed: %s' % par2head)
411 def delete_list (dir, files, interactive=False):
412 # Delete a list of files
414 assert os.path.isdir (dir)
417 valid_y = ['Y', 'YES']
418 valid_n = ['N', 'NO']
422 print 'Do you want to delete the following?:'
423 s = raw_input ('Delete [y/N]: ').upper()
425 if s in valid_y + valid_n:
432 os.remove (os.path.join (dir, f))
437 def generate_all_parsets (dir):
438 # Generate all parsets in the given directory.
440 assert os.path.isdir (dir) # Directory MUST be valid
443 p2files = find_all_par2_files (dir)
452 def check_required_progs():
453 """Check if the required programs are installed"""
455 shell_not_found = 32512
458 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
459 needed.append ('par2repair')
461 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
462 needed.append ('unrar')
464 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
465 needed.append ('unzip')
469 print 'Needed program "%s" not found in $PATH' % (n, )
473 def run_options (options):
476 options.work_dir = full_abspath (options.work_dir)
478 # Make sure that the directory is valid
479 if not os.path.isdir (options.work_dir):
480 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
481 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
482 sys.stderr.write ('configuration file to override the working directory permanently.\n')
485 if options.extract_dir != None:
486 options.extract_dir = full_abspath (options.extract_dir)
489 print PROGRAM + ' - ' + VERSION
491 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
493 print 'This program comes with ABSOLUTELY NO WARRANTY.'
494 print 'This is free software, and you are welcome to redistribute it'
495 print 'under certain conditions. See the file COPYING for details.'
498 if options.check_progs:
499 check_required_progs ()
501 if options.write_def_config:
502 config.write_config (default=True)
504 if options.write_config:
505 config.write_config ()
507 def find_loglevel (options):
509 loglevel = options.verbose - options.quiet
511 if loglevel < RarslaveLogger.MessageType.Fatal:
512 loglevel = RarslaveLogger.MessageType.Fatal
514 if loglevel > RarslaveLogger.MessageType.Debug:
515 loglevel = RarslaveLogger.MessageType.Debug
519 def printMessageTable (loglevel):
521 if logger.hasFatalMessages ():
522 print '\nFatal Messages\n' + '=' * 80
523 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
525 if loglevel == RarslaveLogger.MessageType.Fatal:
528 if logger.hasNormalMessages ():
529 print '\nNormal Messages\n' + '=' * 80
530 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
532 if loglevel == RarslaveLogger.MessageType.Normal:
535 if logger.hasVerboseMessages ():
536 print '\nVerbose Messages\n' + '=' * 80
537 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
539 if loglevel == RarslaveLogger.MessageType.Verbose:
542 if logger.hasDebugMessages ():
543 print '\nDebug Messages\n' + '=' * 80
544 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
550 # Build the OptionParser
551 parser = optparse.OptionParser()
552 parser.add_option('-n', '--not-recursive',
553 action='store_false', dest='recursive',
554 default=config.get_value('options', 'recursive'),
555 help="Don't run recursively")
557 parser.add_option('-d', '--work-dir',
558 dest='work_dir', type='string',
559 default=config.get_value('directories', 'working_directory'),
560 help="Start running at DIR", metavar='DIR')
562 parser.add_option('-e', '--extract-dir',
563 dest='extract_dir', type='string',
564 default=config.get_value('directories', 'extract_directory'),
565 help="Extract to DIR", metavar='DIR')
567 parser.add_option('-p', '--check-required-programs',
568 action='store_true', dest='check_progs',
570 help="Check for required programs")
572 parser.add_option('-f', '--write-default-config',
573 action='store_true', dest='write_def_config',
574 default=False, help="Write out a new default config")
576 parser.add_option('-c', '--write-new-config',
577 action='store_true', dest='write_config',
578 default=False, help="Write out the current config")
580 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
581 default=config.get_value('options', 'interactive'),
582 help="Confirm before removing files")
584 parser.add_option('-q', '--quiet', dest='quiet', action='count',
585 default=0, help="Output fatal messages only")
587 parser.add_option('-v', '--verbose', dest='verbose', action='count',
588 default=0, help="Output extra information")
590 parser.add_option('-V', '--version', dest='version', action='store_true',
591 default=False, help="Output version information")
593 parser.version = VERSION
595 # Parse the given options
597 (options, args) = parser.parse_args()
599 # Run any special actions that are needed on these options
600 run_options (options)
602 # Find the loglevel using the options given
603 loglevel = find_loglevel (options)
606 if options.recursive:
607 for (dir, subdirs, files) in os.walk (options.work_dir):
608 parsets = generate_all_parsets (dir)
614 parsets = generate_all_parsets (options.work_dir)
619 printMessageTable (loglevel)
624 if __name__ == '__main__':