#!/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.
-#
-# Required overrides:
-# ==============================================================================
-# find_extraction_heads ()
-# extraction_function ()
+# When the repair(), extract(), and delete() methods run, all of the class
+# instance variables MUST match the in-filesystem state
#
+# 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
+
+class Base(object):
+
+ # Constructor
+ # @cs an instance of CompareSet
+ def __init__(self, cs, options):
+
+ # Save the options
+ self.options = options
+
+ # The directory and parity file
+ self.directory = cs.directory
+ self.parityFile = cs.parityFile
-class Base (object):
- """Base class for all PAR2Set types"""
+ # The base name of the parity file (for matching)
+ self.baseName = cs.baseName
- # 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
+ # Files that match by name only
+ self.similarlyNamedFiles = cs.similarlyNamedFiles
- def __init__ (self, dir, p2file):
- """Default constructor for all PAR2Set types
+ # PAR2 files from the files matched by name
+ self.PAR2Files = utils.findMatches(r'^.*\.par2', self.similarlyNamedFiles)
- dir -- a directory
- p2file -- a PAR2 file inside the given directory"""
+ # All of the files protected by this set
+ self.protectedFiles = cs.protectedFiles
- assert os.path.isdir (dir)
- assert os.path.isfile (os.path.join (dir, p2file))
+ # Create a set of all the combined files
+ self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles)
- # The real "meat" of the class
- self.dir = dir
- self.p2file = p2file
- self.basename = rsutil.common.get_basename (p2file)
+ # Run the detection
+ # NOTE: Python calls "down" into derived classes automatically
+ # WARNING: This will raise TypeError if it doesn't match
+ self.detect()
- # 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)
+ def __str__(self):
+ return '%s: %s' % (self.__class__.__name__, self.parityFile)
- # 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):
+ return self.allFiles == rhs.allFiles
- 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)
+ # Run all operations
+ def run(self):
- 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."""
+ # Repair Stage
+ try:
+ self.repair()
+ except (CalledProcessError, OSError):
+ logging.critical('Repair stage failed for: %s' % self)
+ raise
- 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)
+ self.updateFilesystemState()
- def runVerifyAndRepair (self):
- """Verify and Repair a PAR2Set. This is done using the par2repair command by
- default"""
+ # Extraction Stage
+ try:
+ self.extract ()
+ except (CalledProcessError, OSError):
+ logging.critical('Extraction stage failed for: %s' % self)
+ raise
- rsutil.common.run_command(['par2repair'] + self.all_p2files, self.dir)
+ self.updateFilesystemState()
- def find_deleteable_files (self):
- """Find all files which are deletable by using the regular expression from the
- configuration file"""
+ # Deletion Stage
+ try:
+ self.delete ()
+ except (CalledProcessError, OSError):
+ logging.critical('Deletion stage failed for: %s' % self)
+ raise
- DELETE_REGEX = rsutil.common.config_get_value ('regular expressions', 'delete_regex')
- dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
+ logging.debug ('Successfully completed: %s' % self)
- 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
+ # Run the repair
+ def repair(self):
- dir -- the directory where the files live
- files -- the filenames themselves
- interactive -- prompt before deleteion"""
+ # This is overly simple, but it works great for almost everything
+ utils.runCommand(['par2repair'] + self.PAR2Files, self.directory)
- assert os.path.isdir (dir)
+ ############################################################################
- # If we are interactive, decide yes or no
- if interactive:
- while True:
- print 'Do you want to delete the following?:'
- for f in files:
- print f
+ # Run the extraction
+ def extract(self):
+ pass
- s = raw_input('Delete [y/N]: ').upper()
+ ############################################################################
- # If we got a valid no answer, leave now
- if s in ['N', 'NO', '']:
- return
+ def findDeletableFiles(self):
- # If we got a valid yes answer, delete them
- if s in ['Y', 'YES']:
- break
+ regex = r'^.*\.(par2|\d|\d\d\d|rar|r\d\d|zip)$'
+ files = utils.findMatches(regex, self.allFiles)
- # Not a good answer, ask again
- print 'Invalid response'
+ return files
- # We got a yes answer (or are non-interactive), delete the files
- for f in files:
+ ############################################################################
- # Get the full filename
- fullname = os.path.join(dir, f)
+ # Run the deletion
+ def delete(self):
- # Delete the file
- try:
- os.remove(fullname)
- logging.debug('Deleting: %s' % fullname)
- except OSError:
- logging.error('Failed to delete: %s' % fullname)
+ # If we aren't to run delete, don't
+ if self.options.delete == False:
+ return
- def runDelete (self):
- """Run the delete operation and return the result"""
+ # Find all deletable files
+ files = self.findDeletableFiles()
+ files.sort()
- deleteable_files = self.find_deleteable_files ()
- ret = self.delete_list_of_files (self.dir, deleteable_files, \
- rsutil.common.options_get_value ('interactive'))
+ # 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
- return ret
+ s = raw_input('Delete [y/N]: ').upper()
- def runAll (self):
- """Run all of the major sections in the class: repair, extraction, and deletion."""
+ # If we got a valid no answer, leave now
+ if s in ['N', 'NO', '']:
+ return
- # Repair Stage
- try:
- self.runVerifyAndRepair ()
- except (RuntimeError, OSError):
- logging.critical('Repair stage failed for: %s' % self.p2file)
- raise
+ # If we got a valid yes answer, delete them
+ if s in ['Y', 'YES']:
+ break
- self.update_matches()
+ # Not a good answer, ask again
+ print 'Invalid response'
- # Extraction Stage
- try:
- self.runExtract ()
- except (RuntimeError, OSError):
- logging.critical('Extraction stage failed for: %s' % self.p2file)
- raise
+ # We got a yes answer (or are non-interactive), delete the files
+ for f in files:
- self.update_matches ()
+ # Get the full filename
+ fullname = os.path.join(self.directory, f)
- # Deletion Stage
- try:
- self.runDelete ()
- except (RuntimeError, OSError):
- logging.critical('Deletion stage failed for: %s' % self.p2file)
- raise
+ # Delete the file
+ try:
+ os.remove(fullname)
+ except OSError:
+ logging.error('Failed to delete: %s' % fullname)
+ else:
+ print 'rm', fullname
+ logging.debug('Deleting: %s' % fullname)
- logging.info ('Successfully completed: %s' % self.p2file)
+ ############################################################################
- def runExtract (self):
- """Extract all heads of this set and return the result"""
+ # 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):
- # 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))
- self.extraction_function (full_head, self.dir)
+ # 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
+ ############################################################################