#!/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
# 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
+ ############################################################################