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 # Make sure that both files are not the same file. If they are, don't run at all.
107 if os.path.samefile (file, os.path.join (todir, file)):
110 cmd = NOEXTRACT_CMD % (file, todir)
111 ret = run_command (cmd)
121 class RarslaveRepairer (object):
122 # Verify (and repair) the set
123 # Make sure it worked, otherwise clean up and return failure
125 def __init__ (self, dir, file, join=False):
126 self.dir = dir # the directory containing the par2 file
127 self.file = file # the par2 file
128 self.join = join # True if the par2 set is 001 002 ...
130 assert os.path.isdir (dir)
131 assert os.path.isfile (os.path.join (dir, file))
133 def checkAndRepair (self):
135 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
136 PAR2_CMD = config.get_value ('commands', 'par2repair')
139 basename = get_basename (self.file)
140 all_files = find_likely_files (basename, self.dir)
142 par2_files = find_par2_files (all_files)
144 # assemble the command
145 command = "%s \"%s\" " % (PAR2_CMD, self.file)
149 command += "\"%s\" " % os.path.split (f)[1]
153 if f not in par2_files:
154 command += "\"%s\" " % os.path.split (f)[1]
157 ret = run_command (command, self.dir)
161 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.file, RarslaveLogger.MessageType.Fatal)
166 def run_command (cmd, indir=None):
167 # Runs the specified command-line in the directory given (or, in the current directory
168 # if none is given). It returns the status code given by the application.
173 assert os.path.isdir (indir) # MUST be a directory!
176 # FIXME: re-enable this after testing
177 print 'RUNNING (%s): %s' % (indir, cmd)
180 # return os.system (cmd)
183 def full_abspath (p):
184 return os.path.abspath (os.path.expanduser (p))
186 def get_basename (name):
187 """Strips most kinds of endings from a filename"""
189 regex = config.get_value ('regular expressions', 'basename_regex')
190 r = re.compile (regex, re.IGNORECASE)
197 g = r.match (name).groups()
203 def find_likely_files (name, dir):
204 """Finds files which are likely to be part of the set corresponding
205 to $name in the directory $dir"""
207 if not os.path.isdir (os.path.abspath (dir)):
208 raise ValueError # bad directory given
210 dir = os.path.abspath (dir)
211 ename = re.escape (name)
212 regex = re.compile ('^%s.*$' % (ename, ))
214 return [f for f in os.listdir (dir) if regex.match (f)]
216 def find_par2_files (files):
217 """Find all par2 files in the list $files"""
219 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
220 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
221 return [f for f in files if regex.match (f)]
223 def find_all_par2_files (dir):
224 """Finds all par2 files in a directory"""
225 # NOTE: does NOT return absolute paths
227 if not os.path.isdir (os.path.abspath (dir)):
228 raise ValueError # bad directory given
230 dir = os.path.abspath (dir)
231 files = os.listdir (dir)
233 return find_par2_files (files)
235 def has_extension (f, ext):
236 """Checks if f has the extension ext"""
241 ext = re.escape (ext)
242 regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE)
243 return regex.match (f)
245 def find_extraction_heads (dir, files):
246 """Takes a list of possible files and finds likely heads of
249 # NOTE: perhaps this should happen AFTER repair is
250 # NOTE: successful. That way all files would already exist
252 # According to various sources online:
253 # 1) pre rar-3.0: .rar .r00 .r01 ...
254 # 2) post rar-3.0: .part01.rar .part02.rar
255 # 3) zip all ver: .zip
258 p2files = find_par2_files (files)
260 # Old RAR type, find all files ending in .rar
261 if is_oldrar (files):
262 extractor = RarslaveExtractor (TYPE_OLDRAR)
263 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
266 extractor.addHead (dir, f)
268 if is_newrar (files):
269 extractor = RarslaveExtractor (TYPE_NEWRAR)
270 regex = re.compile ('^.*\.part01.rar$', re.IGNORECASE)
273 extractor.addHead (dir, f)
276 extractor = RarslaveExtractor (TYPE_ZIP)
277 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
280 extractor.addHead (dir, f)
282 if is_noextract (files):
283 # Use the Par2 Parser (from cfv) here to find out what files are protected.
284 # Since these are not being extracted, they will be mv'd to another directory
286 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
291 prot_files = par2parser.get_protected_files (dir, f)
293 except: #FIXME: add the actual exceptions
294 logger.addMessage ('Error parsing PAR2 file: %s', f)
302 extractor.addHead (dir, f)
304 logger.addMessage ('Error parsing all PAR2 files in this set ...')
306 # Make sure we found the type
307 if extractor == None:
308 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0],
309 RarslaveLogger.MessageType.Fatal)
311 # No-heads here, but it's better than failing completely
312 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
316 def is_oldrar (files):
318 if has_extension (f, '.r00'):
323 def is_newrar (files):
325 if has_extension (f, '.part01.rar'):
332 if has_extension (f, '.zip'):
337 def is_noextract (files):
338 # Type that needs no extraction.
339 # TODO: Add others ???
341 if has_extension (f, '.001'):
346 def find_deleteable_files (files):
347 # Deleteable types regex should come from the config
349 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
350 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
352 return [f for f in files if dregex.match (f)]
358 class PAR2Set (object):
364 def __init__ (self, dir, file):
365 assert os.path.isdir (dir)
366 assert os.path.isfile (os.path.join (dir, file))
371 basename = get_basename (file)
372 self.likely_files = find_likely_files (basename, dir)
374 def __list_eq (self, l1, l2):
376 if len(l1) != len(l2):
385 def __eq__ (self, rhs):
386 return self.__list_eq (self.likely_files, rhs.likely_files)
389 par2files = find_par2_files (self.likely_files)
390 par2head = par2files[0]
392 join = is_noextract (self.likely_files)
395 repairer = RarslaveRepairer (self.dir, par2head, join)
396 ret = repairer.checkAndRepair ()
399 logger.addMessage ('Repair stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
403 EXTRACT_DIR = options.extract_dir
404 extractor = find_extraction_heads (self.dir, self.likely_files)
405 ret = extractor.extract (EXTRACT_DIR)
408 logger.addMessage ('Extraction stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
412 DELETE_INTERACTIVE = options.interactive
413 deleteable_files = find_deleteable_files (self.likely_files)
414 ret = delete_list (deleteable_files, DELETE_INTERACTIVE)
417 logger.addMessage ('Deletion stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
420 logger.addMessage ('Successfully completed: %s' % par2head)
423 def delete_list (files, interactive=False):
424 # Delete a list of files
427 valid_y = ['Y', 'YES']
428 valid_n = ['N', 'NO']
432 print 'Do you want to delete the following?:'
433 s = raw_input ('Delete [y/N]: ').upper()
435 if s in valid_y + valid_n:
442 # FIXME: re-enable this in production
444 print 'rm \"%s\"' % f
449 def generate_all_parsets (dir):
450 # Generate all parsets in the given directory.
452 assert os.path.isdir (dir) # Directory MUST be valid
455 p2files = find_all_par2_files (dir)
464 def check_required_progs():
465 """Check if the required programs are installed"""
467 shell_not_found = 32512
470 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
471 needed.append ('par2repair')
473 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
474 needed.append ('unrar')
476 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
477 needed.append ('unzip')
481 print 'Needed program "%s" not found in $PATH' % (n, )
485 def run_options (options):
488 options.work_dir = full_abspath (options.work_dir)
490 # Make sure that the directory is valid
491 if not os.path.isdir (options.work_dir):
492 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
493 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
494 sys.stderr.write ('configuration file to override the working directory permanently.\n')
497 if options.extract_dir != None:
498 options.extract_dir = full_abspath (options.extract_dir)
501 print PROGRAM + ' - ' + VERSION
503 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
505 print 'This program comes with ABSOLUTELY NO WARRANTY.'
506 print 'This is free software, and you are welcome to redistribute it'
507 print 'under certain conditions. See the file COPYING for details.'
510 if options.check_progs:
511 check_required_progs ()
513 if options.write_def_config:
514 config.write_config (default=True)
516 if options.write_config:
517 config.write_config ()
519 def find_loglevel (options):
521 loglevel = options.verbose - options.quiet
523 if loglevel < RarslaveLogger.MessageType.Fatal:
524 loglevel = RarslaveLogger.MessageType.Fatal
526 if loglevel > RarslaveLogger.MessageType.Debug:
527 loglevel = RarslaveLogger.MessageType.Debug
531 def printMessageTable (loglevel):
533 if logger.hasFatalMessages ():
534 print '\nFatal Messages\n' + '=' * 80
535 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
537 if loglevel == RarslaveLogger.MessageType.Fatal:
540 if logger.hasNormalMessages ():
541 print '\nNormal Messages\n' + '=' * 80
542 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
544 if loglevel == RarslaveLogger.MessageType.Normal:
547 if logger.hasVerboseMessages ():
548 print '\nVerbose Messages\n' + '=' * 80
549 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
551 if loglevel == RarslaveLogger.MessageType.Verbose:
554 if logger.hasDebugMessages ():
555 print '\nDebug Messages\n' + '=' * 80
556 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
562 # Build the OptionParser
563 parser = optparse.OptionParser()
564 parser.add_option('-n', '--not-recursive',
565 action='store_false', dest='recursive',
566 default=config.get_value('options', 'recursive'),
567 help="Don't run recursively")
569 parser.add_option('-d', '--work-dir',
570 dest='work_dir', type='string',
571 default=config.get_value('directories', 'working_directory'),
572 help="Start running at DIR", metavar='DIR')
574 parser.add_option('-e', '--extract-dir',
575 dest='extract_dir', type='string',
576 default=config.get_value('directories', 'extract_directory'),
577 help="Extract to DIR", metavar='DIR')
579 parser.add_option('-p', '--check-required-programs',
580 action='store_true', dest='check_progs',
582 help="Check for required programs")
584 parser.add_option('-f', '--write-default-config',
585 action='store_true', dest='write_def_config',
586 default=False, help="Write out a new default config")
588 parser.add_option('-c', '--write-new-config',
589 action='store_true', dest='write_config',
590 default=False, help="Write out the current config")
592 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
593 default=config.get_value('options', 'interactive'),
594 help="Confirm before removing files")
596 parser.add_option('-q', '--quiet', dest='quiet', action='count',
597 default=0, help="Output fatal messages only")
599 parser.add_option('-v', '--verbose', dest='verbose', action='count',
600 default=0, help="Output extra information")
602 parser.add_option('-V', '--version', dest='version', action='store_true',
603 default=False, help="Output version information")
605 parser.version = VERSION
607 # Parse the given options
609 (options, args) = parser.parse_args()
611 # Run any special actions that are needed on these options
612 run_options (options)
614 # Find the loglevel using the options given
615 loglevel = find_loglevel (options)
618 if options.recursive:
619 for (dir, subdirs, files) in os.walk (options.work_dir):
620 parsets = generate_all_parsets (dir)
626 parsets = generate_all_parsets (options.work_dir)
631 printMessageTable (loglevel)
636 if __name__ == '__main__':