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 (basename, self.dir)
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 return os.system (cmd)
175 def full_abspath (p):
176 return os.path.abspath (os.path.expanduser (p))
178 def get_basename (name):
179 """Strips most kinds of endings from a filename"""
181 regex = config.get_value ('regular expressions', 'basename_regex')
182 r = re.compile (regex, re.IGNORECASE)
189 g = r.match (name).groups()
195 def find_likely_files (name, dir):
196 """Finds files which are likely to be part of the set corresponding
197 to $name in the directory $dir"""
199 if not os.path.isdir (os.path.abspath (dir)):
200 raise ValueError # bad directory given
202 dir = os.path.abspath (dir)
203 ename = re.escape (name)
204 regex = re.compile ('^%s.*$' % (ename, ))
206 return [f for f in os.listdir (dir) if regex.match (f)]
208 def find_par2_files (files):
209 """Find all par2 files in the list $files"""
211 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
212 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
213 return [f for f in files if regex.match (f)]
215 def find_all_par2_files (dir):
216 """Finds all par2 files in a directory"""
217 # NOTE: does NOT return absolute paths
219 if not os.path.isdir (os.path.abspath (dir)):
220 raise ValueError # bad directory given
222 dir = os.path.abspath (dir)
223 files = os.listdir (dir)
225 return find_par2_files (files)
227 def has_extension (f, ext):
228 """Checks if f has the extension ext"""
233 ext = re.escape (ext)
234 regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE)
235 return regex.match (f)
237 def find_extraction_heads (dir, files):
238 """Takes a list of possible files and finds likely heads of
241 # NOTE: perhaps this should happen AFTER repair is
242 # NOTE: successful. That way all files would already exist
244 # According to various sources online:
245 # 1) pre rar-3.0: .rar .r00 .r01 ...
246 # 2) post rar-3.0: .part01.rar .part02.rar
247 # 3) zip all ver: .zip
250 p2files = find_par2_files (files)
252 # Old RAR type, find all files ending in .rar
253 if is_oldrar (files):
254 extractor = RarslaveExtractor (TYPE_OLDRAR)
255 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
258 extractor.addHead (dir, f)
260 if is_newrar (files):
261 extractor = RarslaveExtractor (TYPE_NEWRAR)
262 regex = re.compile ('^.*\.part01.rar$', re.IGNORECASE)
265 extractor.addHead (dir, f)
268 extractor = RarslaveExtractor (TYPE_ZIP)
269 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
272 extractor.addHead (dir, f)
274 if is_noextract (files):
275 # Use the Par2 Parser (from cfv) here to find out what files are protected.
276 # Since these are not being extracted, they will be mv'd to another directory
278 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
283 prot_files = par2parser.get_protected_files (dir, f)
285 except: #FIXME: add the actual exceptions
286 logger.addMessage ('Error parsing PAR2 file: %s', f)
294 extractor.addHead (dir, f)
296 logger.addMessage ('Error parsing all PAR2 files in this set ...')
298 # Make sure we found the type
299 if extractor == None:
300 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0],
301 RarslaveLogger.MessageType.Fatal)
303 # No-heads here, but it's better than failing completely
304 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
308 def is_oldrar (files):
310 if has_extension (f, '.r00'):
315 def is_newrar (files):
317 if has_extension (f, '.part01.rar'):
324 if has_extension (f, '.zip'):
329 def is_noextract (files):
330 # Type that needs no extraction.
331 # TODO: Add others ???
333 if has_extension (f, '.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 (basename, dir)
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 (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 (files, interactive=False):
416 # Delete a list of files
419 valid_y = ['Y', 'YES']
420 valid_n = ['N', 'NO']
424 print 'Do you want to delete the following?:'
425 s = raw_input ('Delete [y/N]: ').upper()
427 if s in valid_y + valid_n:
439 def generate_all_parsets (dir):
440 # Generate all parsets in the given directory.
442 assert os.path.isdir (dir) # Directory MUST be valid
445 p2files = find_all_par2_files (dir)
454 def check_required_progs():
455 """Check if the required programs are installed"""
457 shell_not_found = 32512
460 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
461 needed.append ('par2repair')
463 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
464 needed.append ('unrar')
466 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
467 needed.append ('unzip')
471 print 'Needed program "%s" not found in $PATH' % (n, )
475 def run_options (options):
478 options.work_dir = full_abspath (options.work_dir)
480 # Make sure that the directory is valid
481 if not os.path.isdir (options.work_dir):
482 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
483 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
484 sys.stderr.write ('configuration file to override the working directory permanently.\n')
487 if options.extract_dir != None:
488 options.extract_dir = full_abspath (options.extract_dir)
491 print PROGRAM + ' - ' + VERSION
493 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
495 print 'This program comes with ABSOLUTELY NO WARRANTY.'
496 print 'This is free software, and you are welcome to redistribute it'
497 print 'under certain conditions. See the file COPYING for details.'
500 if options.check_progs:
501 check_required_progs ()
503 if options.write_def_config:
504 config.write_config (default=True)
506 if options.write_config:
507 config.write_config ()
509 def find_loglevel (options):
511 loglevel = options.verbose - options.quiet
513 if loglevel < RarslaveLogger.MessageType.Fatal:
514 loglevel = RarslaveLogger.MessageType.Fatal
516 if loglevel > RarslaveLogger.MessageType.Debug:
517 loglevel = RarslaveLogger.MessageType.Debug
521 def printMessageTable (loglevel):
523 if logger.hasFatalMessages ():
524 print '\nFatal Messages\n' + '=' * 80
525 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
527 if loglevel == RarslaveLogger.MessageType.Fatal:
530 if logger.hasNormalMessages ():
531 print '\nNormal Messages\n' + '=' * 80
532 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
534 if loglevel == RarslaveLogger.MessageType.Normal:
537 if logger.hasVerboseMessages ():
538 print '\nVerbose Messages\n' + '=' * 80
539 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
541 if loglevel == RarslaveLogger.MessageType.Verbose:
544 if logger.hasDebugMessages ():
545 print '\nDebug Messages\n' + '=' * 80
546 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
552 # Build the OptionParser
553 parser = optparse.OptionParser()
554 parser.add_option('-n', '--not-recursive',
555 action='store_false', dest='recursive',
556 default=config.get_value('options', 'recursive'),
557 help="Don't run recursively")
559 parser.add_option('-d', '--work-dir',
560 dest='work_dir', type='string',
561 default=config.get_value('directories', 'working_directory'),
562 help="Start running at DIR", metavar='DIR')
564 parser.add_option('-e', '--extract-dir',
565 dest='extract_dir', type='string',
566 default=config.get_value('directories', 'extract_directory'),
567 help="Extract to DIR", metavar='DIR')
569 parser.add_option('-p', '--check-required-programs',
570 action='store_true', dest='check_progs',
572 help="Check for required programs")
574 parser.add_option('-f', '--write-default-config',
575 action='store_true', dest='write_def_config',
576 default=False, help="Write out a new default config")
578 parser.add_option('-c', '--write-new-config',
579 action='store_true', dest='write_config',
580 default=False, help="Write out the current config")
582 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
583 default=config.get_value('options', 'interactive'),
584 help="Confirm before removing files")
586 parser.add_option('-q', '--quiet', dest='quiet', action='count',
587 default=0, help="Output fatal messages only")
589 parser.add_option('-v', '--verbose', dest='verbose', action='count',
590 default=0, help="Output extra information")
592 parser.add_option('-V', '--version', dest='version', action='store_true',
593 default=False, help="Output version information")
595 parser.version = VERSION
597 # Parse the given options
599 (options, args) = parser.parse_args()
601 # Run any special actions that are needed on these options
602 run_options (options)
604 # Find the loglevel using the options given
605 loglevel = find_loglevel (options)
608 if options.recursive:
609 for (dir, subdirs, files) in os.walk (options.work_dir):
610 parsets = generate_all_parsets (dir)
616 parsets = generate_all_parsets (options.work_dir)
621 printMessageTable (loglevel)
626 if __name__ == '__main__':