Major Update
[rarslave2.git] / PAR2Set / Base.py
index e58cc16..599827e 100644 (file)
 #!/usr/bin/env python
-# vim: set ts=4 sts=4 sw=4 textwidth=92:
+# vim: set ts=4 sts=4 sw=4 textwidth=80:
 
-from RarslaveCommon import *
-import RarslaveLogger
-import Par2Parser
+"""
+Holds the PAR2Set base class
+"""
 
-import re
-import os
-import logging
+__author__    = "Ira W. Snyder (devel@irasnyder.com)"
+__copyright__ = "Copyright (c) 2006,2007 Ira W. Snyder (devel@irasnyder.com)"
+__license__   = "GNU GPL v2 (or, at your option, any later version)"
 
-# 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.
+#    Base.py
 #
-# 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.
+#    Copyright (C) 2006,2007  Ira W. Snyder (devel@irasnyder.com)
 #
-# Assumptions made about each of the run*() functions:
-# ==============================================================================
-# The state of self.name_matched_files and self.prot_matched_files will be consistent
-# with the real, in-filesystem state at the time that they are called.
-# (This is why runAll() calls update_matches() all the time.)
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
 #
-# Required overrides:
-# ==============================================================================
-# find_extraction_heads ()
-# extraction_function ()
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
 #
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-class PAR2Set (object):
+import re, os, logging
+from subprocess import CalledProcessError
+from PAR2Set import CompareSet, utils
 
-       # 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
+# 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.
+#
+# 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
 
-       def __init__ (self, dir, p2file):
-               assert os.path.isdir (dir)
-               assert os.path.isfile (os.path.join (dir, p2file))
+class Base(object):
 
-               # The real "meat" of the class
-               self.dir = dir
-               self.p2file = p2file
-               self.basename = get_basename (p2file)
+    # Constructor
+    # @cs an instance of CompareSet
+    def __init__(self, cs, options):
 
-               # Find files that match by name only
-               self.name_matched_files = find_name_matches (self.dir, self.basename)
+        # Save the options
+        self.options = options
 
-               # Find all par2 files for this set using name matches
-               self.all_p2files = find_par2_files (self.name_matched_files)
+        # The directory and parity file
+        self.directory = cs.directory
+        self.parityFile = cs.parityFile
 
-               # Try to get the protected files for this set
-               self.prot_matched_files = parse_all_par2 (self.dir, self.p2file, self.all_p2files)
+        # The base name of the parity file (for matching)
+        self.baseName = cs.baseName
 
-               # Setup the all_files combined set (for convenience only)
-               self.all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
+        # Files that match by name only
+        self.similarlyNamedFiles = cs.similarlyNamedFiles
 
-       def __eq__ (self, rhs):
-               return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \
-                               list_eq (self.name_matched_files, rhs.name_matched_files) and \
-                               list_eq (self.prot_matched_files, rhs.prot_matched_files)
+        # PAR2 files from the files matched by name
+        self.PAR2Files = utils.findMatches(r'^.*\.par2', self.similarlyNamedFiles)
 
-       def update_matches (self):
-               """Updates the contents of instance variables which are likely to change after
-               running an operation, usually one which will create new files."""
+        # All of the files protected by this set
+        self.protectedFiles = cs.protectedFiles
 
-               self.name_matched_files = find_name_matches (self.dir, self.basename)
-               self.all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
+        # Create a set of all the combined files
+        self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles)
 
-       def runVerifyAndRepair (self):
-               PAR2_CMD = config_get_value ('commands', 'par2repair')
+        # Run the detection
+        # NOTE: Python calls "down" into derived classes automatically
+        # WARNING: This will raise TypeError if it doesn't match
+        self.detect()
 
-               # assemble the command
-               # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
-               command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
+    ############################################################################
 
-               for f in self.all_p2files:
-                       if f != self.p2file:
-                               command += "\"%s\" " % os.path.split (f)[1]
+    def __str__(self):
+        return '%s: %s' % (self.__class__.__name__, self.parityFile)
 
-               # run the command
-               ret = run_command (command, self.dir)
+    ############################################################################
 
-               # check the result
-               if ret != 0:
-                       logging.critical ('PAR2 Check / Repair failed: %s' % self.p2file)
-                       return -ECHECK
+    def __eq__(self, rhs):
+        return self.allFiles == rhs.allFiles
 
-               return SUCCESS
+    ############################################################################
 
-       def find_deleteable_files (self):
-               DELETE_REGEX = config_get_value ('regular expressions', 'delete_regex')
-               dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
+    # Run all operations
+    def run(self):
 
-               return [f for f in self.all_files if dregex.match (f)]
+        # Repair Stage
+        try:
+            self.repair()
+        except (CalledProcessError, OSError):
+            logging.critical('Repair stage failed for: %s' % self)
+            raise
 
-       def delete_list_of_files (self, dir, files, interactive=False):
-               # Delete a list of files
+        self.updateFilesystemState()
 
-               assert os.path.isdir (dir)
+        # Extraction Stage
+        try:
+            self.extract ()
+        except (CalledProcessError, OSError):
+            logging.critical('Extraction stage failed for: %s' % self)
+            raise
 
-               done = False
-               valid_y = ['Y', 'YES']
-               valid_n = ['N', 'NO', '']
+        self.updateFilesystemState()
 
-               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()
+        # Deletion Stage
+        try:
+            self.delete ()
+        except (CalledProcessError, OSError):
+            logging.critical('Deletion stage failed for: %s' % self)
+            raise
 
-                               if s in valid_y + valid_n:
-                                       done = True
+        logging.debug ('Successfully completed: %s' % self)
 
-                       if s in valid_n:
-                               return SUCCESS
+    ############################################################################
 
-               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 -EDELETE
+    # Run the repair
+    def repair(self):
 
-               return SUCCESS
+        # This is overly simple, but it works great for almost everything
+        utils.runCommand(['par2repair'] + self.PAR2Files, self.directory)
 
-       def runDelete (self):
-               deleteable_files = self.find_deleteable_files ()
-               ret = self.delete_list_of_files (self.dir, deleteable_files, \
-                               options_get_value ('interactive'))
+    ############################################################################
 
-               return ret
+    # Run the extraction
+    def extract(self):
+        pass
 
-       def runAll (self):
+    ############################################################################
 
-               # Repair Stage
-               ret = self.runVerifyAndRepair ()
+    def findDeletableFiles(self):
 
-               if ret != SUCCESS:
-                       logging.critical ('Repair stage failed for: %s' % self.p2file)
-                       return -ECHECK
+        regex = r'^.*\.(par2|\d|\d\d\d|rar|r\d\d|zip)$'
+        files = utils.findMatches(regex, self.allFiles)
 
-               self.update_matches ()
+        return files
 
-               # Extraction Stage
-               ret = self.runExtract ()
+    ############################################################################
 
-               if ret != SUCCESS:
-                       logging.critical ('Extraction stage failed for: %s' % self.p2file)
-                       return -EEXTRACT
+    # Run the deletion
+    def delete(self):
 
-               self.update_matches ()
+        # If we aren't to run delete, don't
+        if self.options.delete == False:
+            return
 
-               # Deletion Stage
-               ret = self.runDelete ()
+        # Find all deletable files
+        files = self.findDeletableFiles()
+        files.sort()
 
-               if ret != SUCCESS:
-                       logging.critical ('Deletion stage failed for: %s' % self.p2file)
-                       return -EDELETE
+        # 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
 
-               logging.info ('Successfully completed: %s' % self.p2file)
-               return SUCCESS
+                s = raw_input('Delete [y/N]: ').upper()
 
-       def safe_create_directory (self, dir):
-               if dir == None:
-                       return SUCCESS
+                # If we got a valid no answer, leave now
+                if s in ['N', 'NO', '']:
+                    return
 
-               if os.path.isdir (dir):
-                       return SUCCESS
+                # If we got a valid yes answer, delete them
+                if s in ['Y', 'YES']:
+                    break
 
-               try:
-                       os.makedirs (dir)
-                       logging.info ('Created directory: %s' % dir)
-               except OSError:
-                       logging.critical ('FAILED to create directory: %s' % dir)
-                       return -ECREATE
+                # Not a good answer, ask again
+                print 'Invalid response'
 
-               return SUCCESS
+        # We got a yes answer (or are non-interactive), delete the files
+        for f in files:
 
-       def runExtract (self, todir=None):
-               """Extract all heads of this set"""
+            # Get the full filename
+            fullname = os.path.join(self.directory, f)
 
-               # Extract to the head's dir if we don't care where to extract
-               if todir == None:
-                       todir = self.dir
+            # Delete the file
+            try:
+                os.remove(fullname)
+                print 'rm', fullname
+                logging.debug('Deleting: %s' % fullname)
+            except OSError:
+                logging.error('Failed to delete: %s' % fullname)
 
-               # Create the directory $todir if it doesn't exist
-               ret = self.safe_create_directory (todir)
+    ############################################################################
 
-               if ret != SUCCESS:
-                       return -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):
 
-               # Call the extraction function on each head
-               for h in self.find_extraction_heads ():
-                       full_head = full_abspath (os.path.join (self.dir, h))
-                       ret = self.extraction_function (full_head, todir)
-                       logging.debug ('Extraction Function returned: %d' % ret)
+        # This must be overridden by the subclasses that actually implement
+        # the functionality of rarslave
 
-                       # Check error code
-                       if ret != SUCCESS:
-                               logging.critical ('Failed extracting: %s' % h)
-                               return -EEXTRACT
+        # The original implementation used ONLY the following here:
+        # self.similarlyNamedFiles
+        # self.protectedFiles
+        raise TypeError
 
-               return SUCCESS
+    ############################################################################
 
-       def find_extraction_heads (self):
-               assert False # You MUST override this on a per-type basis
+    # 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):
 
-       def extraction_function (self, file, todir):
-               # NOTE: Please keep the prototype the same for all overridden functions.
-               # Doing so will guarantee that your life is made much easier.
-               #
-               # Also note that the todir given will always be valid for the current directory
-               # when the function is called.
+        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
+    ############################################################################