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 # REQUIRES that the dir is valid, but not that the file is valid, so that
30 # we can move a file that doesn't exist yet.
31 # FIXME: probably CAN add this back, since we should be running this AFTER repair.
32 #assert os.path.isfile (os.path.join (dir, head))
34 full_head = os.path.join (dir, head)
35 logger.addMessage ('Adding extraction head: %s' % full_head, RarslaveLogger.MessageType.Debug)
36 self.heads.append (full_head)
38 def extract (self, todir=None):
39 # Extract all heads of this set
41 # Create the directory $todir if it doesn't exist
42 if todir != None and not os.path.isdir (todir):
43 logger.addMessage ('Creating directory: %s' % todir, RarslaveLogger.MessageType.Verbose)
47 logger.addMessage ('FAILED to create directory: %s' % todir, RarslaveLogger.MessageType.Fatal)
52 { TYPE_OLDRAR : self.__extract_rar,
53 TYPE_NEWRAR : self.__extract_rar,
54 TYPE_ZIP : self.__extract_zip,
55 TYPE_NOEXTRACT : self.__extract_noextract }[self.type]
57 # Call the extraction function on each head
60 # Run in the head's directory
61 ret = extraction_func (h, os.path.dirname (h))
63 ret = extraction_func (h, todir)
65 logger.addMessage ('Extraction Function returned: %d' % ret, RarslaveLogger.MessageType.Debug)
69 logger.addMessage ('Failed extracting: %s' % h, RarslaveLogger.MessageType.Fatal)
74 def __extract_rar (self, file, todir):
75 assert os.path.isfile (file)
76 assert os.path.isdir (todir)
78 RAR_CMD = config.get_value ('commands', 'unrar')
80 cmd = '%s \"%s\"' % (RAR_CMD, file)
81 ret = run_command (cmd, todir)
89 def __extract_zip (self, file, todir):
90 ZIP_CMD = config.get_value ('commands', 'unzip')
92 cmd = ZIP_CMD % (file, todir)
93 ret = run_command (cmd)
101 def __extract_noextract (self, file, todir):
102 # Just move this file to the $todir, since no extraction is needed
103 # FIXME: NOTE: mv will fail by itself if you're moving to the same dir!
104 NOEXTRACT_CMD = config.get_value ('commands', 'noextract')
106 cmd = NOEXTRACT_CMD % (file, todir)
107 ret = run_command (cmd)
117 class RarslaveRepairer (object):
118 # Verify (and repair) the set
119 # Make sure it worked, otherwise clean up and return failure
121 def __init__ (self, dir, file, join=False):
122 self.dir = dir # the directory containing the par2 file
123 self.file = file # the par2 file
124 self.join = join # True if the par2 set is 001 002 ...
126 assert os.path.isdir (dir)
127 assert os.path.isfile (os.path.join (dir, file))
129 def checkAndRepair (self):
131 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
132 PAR2_CMD = config.get_value ('commands', 'par2repair')
135 basename = get_basename (self.file)
136 all_files = find_likely_files (basename, self.dir)
138 par2_files = find_par2_files (all_files)
140 # assemble the command
141 command = "%s \"%s\" " % (PAR2_CMD, self.file)
145 command += "\"%s\" " % os.path.split (f)[1]
149 if f not in par2_files:
150 command += "\"%s\" " % os.path.split (f)[1]
153 ret = run_command (command, self.dir)
157 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.file, RarslaveLogger.MessageType.Fatal)
162 def run_command (cmd, indir=None):
163 # Runs the specified command-line in the directory given (or, in the current directory
164 # if none is given). It returns the status code given by the application.
169 assert os.path.isdir (indir) # MUST be a directory!
172 # FIXME: re-enable this after testing
173 print 'RUNNING (%s): %s' % (indir, cmd)
176 # return os.system (cmd)
179 def full_abspath (p):
180 return os.path.abspath (os.path.expanduser (p))
182 def get_basename (name):
183 """Strips most kinds of endings from a filename"""
185 regex = config.get_value ('regular expressions', 'basename_regex')
186 r = re.compile (regex, re.IGNORECASE)
193 g = r.match (name).groups()
199 def find_likely_files (name, dir):
200 """Finds files which are likely to be part of the set corresponding
201 to $name in the directory $dir"""
203 if not os.path.isdir (os.path.abspath (dir)):
204 raise ValueError # bad directory given
206 dir = os.path.abspath (dir)
207 ename = re.escape (name)
208 regex = re.compile ('^%s.*$' % (ename, ))
210 return [f for f in os.listdir (dir) if regex.match (f)]
212 def find_par2_files (files):
213 """Find all par2 files in the list $files"""
215 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
216 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
217 return [f for f in files if regex.match (f)]
219 def find_all_par2_files (dir):
220 """Finds all par2 files in a directory"""
221 # NOTE: does NOT return absolute paths
223 if not os.path.isdir (os.path.abspath (dir)):
224 raise ValueError # bad directory given
226 dir = os.path.abspath (dir)
227 files = os.listdir (dir)
229 return find_par2_files (files)
231 def has_extension (f, ext):
232 """Checks if f has the extension ext"""
237 ext = re.escape (ext)
238 regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE)
239 return regex.match (f)
241 def find_extraction_heads (dir, files):
242 """Takes a list of possible files and finds likely heads of
245 # NOTE: perhaps this should happen AFTER repair is
246 # NOTE: successful. That way all files would already exist
248 # According to various sources online:
249 # 1) pre rar-3.0: .rar .r00 .r01 ...
250 # 2) post rar-3.0: .part01.rar .part02.rar
251 # 3) zip all ver: .zip
254 p2files = find_par2_files (files)
256 # Old RAR type, find all files ending in .rar
257 if is_oldrar (files):
258 extractor = RarslaveExtractor (TYPE_OLDRAR)
259 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
262 extractor.addHead (dir, f)
264 if is_newrar (files):
265 extractor = RarslaveExtractor (TYPE_NEWRAR)
266 regex = re.compile ('^.*\.part01.rar$', re.IGNORECASE)
269 extractor.addHead (dir, f)
272 extractor = RarslaveExtractor (TYPE_ZIP)
273 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
276 extractor.addHead (dir, f)
278 if is_noextract (files):
279 # Use the Par2 Parser (from cfv) here to find out what files are protected.
280 # Since these are not being extracted, they will be mv'd to another directory
282 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
287 prot_files = par2parser.get_protected_files (dir, f)
289 except: #FIXME: add the actual exceptions
290 logger.addMessage ('Error parsing PAR2 file: %s', f)
298 extractor.addHead (dir, f)
300 logger.addMessage ('Error parsing all PAR2 files in this set ...')
302 # Make sure we found the type
303 if extractor == None:
304 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0],
305 RarslaveLogger.MessageType.Fatal)
307 # No-heads here, but it's better than failing completely
308 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
312 def is_oldrar (files):
314 if has_extension (f, '.r00'):
319 def is_newrar (files):
321 if has_extension (f, '.part01.rar'):
328 if has_extension (f, '.zip'):
333 def is_noextract (files):
334 # Type that needs no extraction.
335 # TODO: Add others ???
337 if has_extension (f, '.001'):
342 def find_deleteable_files (files):
343 # Deleteable types regex should come from the config
345 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
346 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
348 return [f for f in files if dregex.match (f)]
354 class PAR2Set (object):
360 def __init__ (self, dir, file):
361 assert os.path.isdir (dir)
362 assert os.path.isfile (os.path.join (dir, file))
367 basename = get_basename (file)
368 self.likely_files = find_likely_files (basename, dir)
370 def __list_eq (self, l1, l2):
372 if len(l1) != len(l2):
381 def __eq__ (self, rhs):
382 return self.__list_eq (self.likely_files, rhs.likely_files)
385 par2files = find_par2_files (self.likely_files)
386 par2head = par2files[0]
388 join = is_noextract (self.likely_files)
391 repairer = RarslaveRepairer (self.dir, par2head, join)
392 ret = repairer.checkAndRepair ()
395 logger.addMessage ('Repair stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
399 EXTRACT_DIR = options.extract_dir
400 extractor = find_extraction_heads (self.dir, self.likely_files)
401 ret = extractor.extract (EXTRACT_DIR)
404 logger.addMessage ('Extraction stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
408 DELETE_INTERACTIVE = options.interactive
409 deleteable_files = find_deleteable_files (self.likely_files)
410 ret = delete_list (deleteable_files, DELETE_INTERACTIVE)
413 logger.addMessage ('Deletion stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
416 logger.addMessage ('Successfully completed: %s' % par2head)
419 def delete_list (files, interactive=False):
420 # Delete a list of files
423 valid_y = ['Y', 'YES']
424 valid_n = ['N', 'NO']
428 print 'Do you want to delete the following?:'
429 s = raw_input ('Delete [y/N]: ').upper()
431 if s in valid_y + valid_n:
438 # FIXME: re-enable this in production
440 print 'rm \"%s\"' % f
445 def generate_all_parsets (dir):
446 # Generate all parsets in the given directory.
448 assert os.path.isdir (dir) # Directory MUST be valid
451 p2files = find_all_par2_files (dir)
460 def check_required_progs():
461 """Check if the required programs are installed"""
463 shell_not_found = 32512
466 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
467 needed.append ('par2repair')
469 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
470 needed.append ('unrar')
472 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
473 needed.append ('unzip')
477 print 'Needed program "%s" not found in $PATH' % (n, )
481 def run_options (options):
482 options.work_dir = full_abspath (options.work_dir)
484 if options.extract_dir != None:
485 options.extract_dir = full_abspath (options.extract_dir)
488 print PROGRAM + ' - ' + VERSION
490 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
492 print 'This program comes with ABSOLUTELY NO WARRANTY.'
493 print 'This is free software, and you are welcome to redistribute it'
494 print 'under certain conditions. See the file COPYING for details.'
497 if options.check_progs:
498 check_required_progs ()
500 if options.write_def_config:
501 config.write_config (default=True)
503 if options.write_config:
504 config.write_config ()
506 def find_loglevel (options):
508 loglevel = options.verbose - options.quiet
510 if loglevel < RarslaveLogger.MessageType.Fatal:
511 loglevel = RarslaveLogger.MessageType.Fatal
513 if loglevel > RarslaveLogger.MessageType.Debug:
514 loglevel = RarslaveLogger.MessageType.Debug
518 def printMessageTable (loglevel):
520 if logger.hasFatalMessages ():
521 print '\nFatal Messages\n' + '=' * 80
522 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
524 if loglevel == RarslaveLogger.MessageType.Fatal:
527 if logger.hasNormalMessages ():
528 print '\nNormal Messages\n' + '=' * 80
529 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
531 if loglevel == RarslaveLogger.MessageType.Normal:
534 if logger.hasVerboseMessages ():
535 print '\nVerbose Messages\n' + '=' * 80
536 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
538 if loglevel == RarslaveLogger.MessageType.Verbose:
541 if logger.hasDebugMessages ():
542 print '\nDebug Messages\n' + '=' * 80
543 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
549 # Build the OptionParser
550 parser = optparse.OptionParser()
551 parser.add_option('-n', '--not-recursive',
552 action='store_false', dest='recursive',
553 default=config.get_value('options', 'recursive'),
554 help="Don't run recursively")
556 parser.add_option('-d', '--work-dir',
557 dest='work_dir', type='string',
558 default=config.get_value('directories', 'working_directory'),
559 help="Start running at DIR", metavar='DIR')
561 parser.add_option('-e', '--extract-dir',
562 dest='extract_dir', type='string',
563 default=config.get_value('directories', 'extract_directory'),
564 help="Extract to DIR", metavar='DIR')
566 parser.add_option('-p', '--check-required-programs',
567 action='store_true', dest='check_progs',
569 help="Check for required programs")
571 parser.add_option('-f', '--write-default-config',
572 action='store_true', dest='write_def_config',
573 default=False, help="Write out a new default config")
575 parser.add_option('-c', '--write-new-config',
576 action='store_true', dest='write_config',
577 default=False, help="Write out the current config")
579 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
580 default=config.get_value('options', 'interactive'),
581 help="Confirm before removing files")
583 parser.add_option('-q', '--quiet', dest='quiet', action='count',
584 default=0, help="Output fatal messages only")
586 parser.add_option('-v', '--verbose', dest='verbose', action='count',
587 default=0, help="Output extra information")
589 parser.add_option('-V', '--version', dest='version', action='store_true',
590 default=False, help="Output version information")
592 parser.version = VERSION
594 # Parse the given options
596 (options, args) = parser.parse_args()
598 # Run any special actions that are needed on these options
599 run_options (options)
601 # Find the loglevel using the options given
602 loglevel = find_loglevel (options)
605 if options.recursive:
606 for (dir, subdirs, files) in os.walk (options.work_dir):
607 parsets = generate_all_parsets (dir)
613 parsets = generate_all_parsets (options.work_dir)
618 printMessageTable (loglevel)
623 if __name__ == '__main__':