X-Git-Url: https://www.irasnyder.com/gitweb/?p=rarslave2.git;a=blobdiff_plain;f=PAR2Set%2FBase.py;h=599827e60ac066fc44e3e5644540b26886f52972;hp=533ff4f53717d26b379e83f757f588b04f750dfc;hb=feeefeb8ea2f1e4724424d43c0eb872aee4743c2;hpb=44cd23f16a5a184adb1d8890633a0e432a2e5636 diff --git a/PAR2Set/Base.py b/PAR2Set/Base.py index 533ff4f..599827e 100644 --- a/PAR2Set/Base.py +++ b/PAR2Set/Base.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# vim: set ts=4 sts=4 sw=4 textwidth=92: +# vim: set ts=4 sts=4 sw=4 textwidth=80: """ Holds the PAR2Set base class @@ -27,250 +27,196 @@ __license__ = "GNU GPL v2 (or, at your option, any later version)" # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -import re -import os -import logging +import re, os, logging +from subprocess import CalledProcessError +from PAR2Set import CompareSet, utils -import rsutil.common - -# This is a fairly generic class which does all of the major things that a PAR2 -# set will need to have done to be verified and extracted. For most "normal" types -# you won't need to override hardly anything. -# -# It is ok to override other functions if the need arises, just make sure that you -# understand why things are done the way that they are in the original versions. +# This is a generic class which handles all of the major operations that a PAR2 +# set will need to be repaired, extracted, and cleaned up. For most subclasses, +# you shouldn't need to do very much. # -# Assumptions made in the runVerifyAndRepair(), runExtract() and runDelete() functions: -# ============================================================================== -# The state of self.name_matched_files, self.prot_matched_files, and self.all_files -# will be consistent with the real, in-filesystem state at the time that they are -# called. This is the reason that runAll() calls update_matches() after running each -# operation that will possibly change the filesystem. +# When the repair(), extract(), and delete() methods run, all of the class +# instance variables MUST match the in-filesystem state # -# Required overrides: -# ============================================================================== -# find_extraction_heads () -# extraction_function () -# - -class Base (object): - """Base class for all PAR2Set types""" - - # Instance Variables - # ========================================================================== - # dir -- The directory this set lives in - # p2file -- The starting PAR2 file - # basename -- The basename of the set, guessed from the PAR2 file - # all_p2files -- All PAR2 files of the set, guessed from the PAR2 file name only - # name_matched_files -- Files in this set, guessed by name only - # prot_matched_files -- Files in this set, guessed by parsing the PAR2 only - - def __init__ (self, dir, p2file): - """Default constructor for all PAR2Set types - - dir -- a directory - p2file -- a PAR2 file inside the given directory""" - - assert os.path.isdir (dir) - assert os.path.isfile (os.path.join (dir, p2file)) - - # The real "meat" of the class - self.dir = dir - self.p2file = p2file - self.basename = rsutil.common.get_basename (p2file) - - # Find files that match by name only - self.name_matched_files = rsutil.common.find_name_matches (self.dir, self.basename) - - # Find all par2 files for this set using name matches - self.all_p2files = rsutil.common.find_par2_files (self.name_matched_files) - - # Try to get the protected files for this set - self.prot_matched_files = rsutil.common.parse_all_par2 (self.dir, self.p2file, self.all_p2files) - - # Setup the all_files combined set (for convenience only) - self.all_files = rsutil.common.no_duplicates (self.name_matched_files + self.prot_matched_files) - - def __eq__ (self, rhs): - """Check for equality between PAR2Set types""" - - return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \ - rsutil.common.list_eq (self.name_matched_files, rhs.name_matched_files) and \ - rsutil.common.list_eq (self.prot_matched_files, rhs.prot_matched_files) - - def update_matches (self): - """Updates the contents of instance variables with the current in-filesystem state. - This should be run after any operation which can create or delete files.""" +# To create a new subclass: +# 1) Override the detect() method to detect your set type, raise TypeError if +# this is not your type +# 2) Optionally, override extract() to extract your set +# 3) Override any other methods where your set differs from the Base +# implementation - self.name_matched_files = rsutil.common.find_name_matches (self.dir, self.basename) - self.all_files = rsutil.common.no_duplicates (self.name_matched_files + self.prot_matched_files) +class Base(object): - def runVerifyAndRepair (self): - """Verify and Repair a PAR2Set. This is done using the par2repair command by - default""" + # Constructor + # @cs an instance of CompareSet + def __init__(self, cs, options): - PAR2_CMD = rsutil.common.config_get_value ('commands', 'par2repair') + # Save the options + self.options = options - # assemble the command - # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES] - command = "%s \"%s\" " % (PAR2_CMD, self.p2file) + # The directory and parity file + self.directory = cs.directory + self.parityFile = cs.parityFile - for f in self.all_p2files: - if f != self.p2file: - command += "\"%s\" " % os.path.split (f)[1] + # The base name of the parity file (for matching) + self.baseName = cs.baseName - # run the command - ret = rsutil.common.run_command (command, self.dir) + # Files that match by name only + self.similarlyNamedFiles = cs.similarlyNamedFiles - # check the result - if ret != 0: - logging.critical ('PAR2 Check / Repair failed: %s' % self.p2file) - return -rsutil.common.ECHECK + # PAR2 files from the files matched by name + self.PAR2Files = utils.findMatches(r'^.*\.par2', self.similarlyNamedFiles) - return rsutil.common.SUCCESS + # All of the files protected by this set + self.protectedFiles = cs.protectedFiles - def find_deleteable_files (self): - """Find all files which are deletable by using the regular expression from the - configuration file""" + # Create a set of all the combined files + self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles) - DELETE_REGEX = rsutil.common.config_get_value ('regular expressions', 'delete_regex') - dregex = re.compile (DELETE_REGEX, re.IGNORECASE) + # Run the detection + # NOTE: Python calls "down" into derived classes automatically + # WARNING: This will raise TypeError if it doesn't match + self.detect() - return [f for f in self.all_files if dregex.match (f)] + ############################################################################ - def delete_list_of_files (self, dir, files, interactive=False): - """Attempt to delete all files given + def __str__(self): + return '%s: %s' % (self.__class__.__name__, self.parityFile) - dir -- the directory where the files live - files -- the filenames themselves - interactive -- prompt before deleteion""" + ############################################################################ - assert os.path.isdir (dir) + def __eq__(self, rhs): + return self.allFiles == rhs.allFiles - done = False - valid_y = ['Y', 'YES'] - valid_n = ['N', 'NO', ''] + ############################################################################ - if interactive: - while not done: - print 'Do you want to delete the following?:' - for f in files: - print f - s = raw_input ('Delete [y/N]: ').upper() + # Run all operations + def run(self): - if s in valid_y + valid_n: - done = True + # Repair Stage + try: + self.repair() + except (CalledProcessError, OSError): + logging.critical('Repair stage failed for: %s' % self) + raise - if s in valid_n: - return rsutil.common.SUCCESS + self.updateFilesystemState() - for f in files: - try: - os.remove (os.path.join (dir, f)) - logging.debug ('Deleteing: %s' % os.path.join (dir, f)) - except: - logging.error ('Failed to delete: %s' % os.path.join (dir, f)) - return -rsutil.common.EDELETE + # Extraction Stage + try: + self.extract () + except (CalledProcessError, OSError): + logging.critical('Extraction stage failed for: %s' % self) + raise - return rsutil.common.SUCCESS + self.updateFilesystemState() - def runDelete (self): - """Run the delete operation and return the result""" + # Deletion Stage + try: + self.delete () + except (CalledProcessError, OSError): + logging.critical('Deletion stage failed for: %s' % self) + raise - deleteable_files = self.find_deleteable_files () - ret = self.delete_list_of_files (self.dir, deleteable_files, \ - rsutil.common.options_get_value ('interactive')) + logging.debug ('Successfully completed: %s' % self) - return ret + ############################################################################ - def runAll (self): - """Run all of the major sections in the class: repair, extraction, and deletion.""" + # Run the repair + def repair(self): - # Repair Stage - ret = self.runVerifyAndRepair () + # This is overly simple, but it works great for almost everything + utils.runCommand(['par2repair'] + self.PAR2Files, self.directory) - if ret != rsutil.common.SUCCESS: - logging.critical ('Repair stage failed for: %s' % self.p2file) - return -rsutil.common.ECHECK + ############################################################################ - self.update_matches () + # Run the extraction + def extract(self): + pass - # Extraction Stage - ret = self.runExtract () + ############################################################################ - if ret != rsutil.common.SUCCESS: - logging.critical ('Extraction stage failed for: %s' % self.p2file) - return -rsutil.common.EEXTRACT + def findDeletableFiles(self): - self.update_matches () + regex = r'^.*\.(par2|\d|\d\d\d|rar|r\d\d|zip)$' + files = utils.findMatches(regex, self.allFiles) - # Deletion Stage - ret = self.runDelete () + return files - if ret != rsutil.common.SUCCESS: - logging.critical ('Deletion stage failed for: %s' % self.p2file) - return -rsutil.common.EDELETE + ############################################################################ - logging.info ('Successfully completed: %s' % self.p2file) - return rsutil.common.SUCCESS + # Run the deletion + def delete(self): - def safe_create_directory (self, dir): - """Safely create a directory, logging the result. + # If we aren't to run delete, don't + if self.options.delete == False: + return - dir -- the directory to create (None is ignored)""" + # Find all deletable files + files = self.findDeletableFiles() + files.sort() - if dir == None: - return rsutil.common.SUCCESS + # If we are interactive, get the user's response, 'yes' or 'no' + if self.options.interactive: + while True: + print 'Do you want to delete the following?:' + for f in files: + print f - if os.path.isdir (dir): - return rsutil.common.SUCCESS + s = raw_input('Delete [y/N]: ').upper() - try: - os.makedirs (dir) - logging.info ('Created directory: %s' % dir) - except OSError: - logging.critical ('FAILED to create directory: %s' % dir) - return -rsutil.common.ECREATE + # If we got a valid no answer, leave now + if s in ['N', 'NO', '']: + return - return rsutil.common.SUCCESS + # If we got a valid yes answer, delete them + if s in ['Y', 'YES']: + break - def runExtract (self, todir=None): - """Extract all heads of this set and return the result""" + # Not a good answer, ask again + print 'Invalid response' - # Extract to the head's dir if we don't care where to extract - if todir == None: - todir = self.dir + # We got a yes answer (or are non-interactive), delete the files + for f in files: - # Create the directory $todir if it doesn't exist - ret = self.safe_create_directory (todir) + # Get the full filename + fullname = os.path.join(self.directory, f) - if ret != rsutil.common.SUCCESS: - return -rsutil.common.EEXTRACT + # Delete the file + try: + os.remove(fullname) + print 'rm', fullname + logging.debug('Deleting: %s' % fullname) + except OSError: + logging.error('Failed to delete: %s' % fullname) - # Call the extraction function on each head - for h in self.find_extraction_heads (): - full_head = rsutil.common.full_abspath (os.path.join (self.dir, h)) - ret = self.extraction_function (full_head, todir) - logging.debug ('Extraction Function returned: %d' % ret) + ############################################################################ - # Check error code - if ret != rsutil.common.SUCCESS: - logging.critical ('Failed extracting: %s' % h) - return -rsutil.common.EEXTRACT + # Detect if the given files make up a set of this type + # + # Raise a TypeError if there is no match. This is meant to be called + # from the constructor, from which we cannot return a value... + def detect(self): - return rsutil.common.SUCCESS + # This must be overridden by the subclasses that actually implement + # the functionality of rarslave - def find_extraction_heads (self): - """Find all extraction heads associated with this set. This must be - overridden for the associated PAR2Set derived class to work.""" + # The original implementation used ONLY the following here: + # self.similarlyNamedFiles + # self.protectedFiles + raise TypeError - assert False # You MUST override this on a per-type basis + ############################################################################ - def extraction_function (self, file, todir): - """Extract a single head of this PAR2Set's type. + # Update the class' state to match the current filesystem state + # + # This must be called after any operation which can create or delete files + # which are relevant to repair(), extract(), and delete() + def updateFilesystemState(self): - file -- the full path to the file to be extracted - todir -- the directory to extract to""" + self.similarlyNamedFiles = utils.findFileNameMatches(self.directory, + self.baseName) + self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles) - assert False # You MUST override this on a per-type basis + ############################################################################