Use exceptions for error handling
[rarslave2.git] / PAR2Set / Base.py
index 012c01a..a5d3dcb 100644 (file)
@@ -1,13 +1,38 @@
 #!/usr/bin/env python
 # vim: set ts=4 sts=4 sw=4 textwidth=92:
 
-from RarslaveCommon import *
-import Par2Parser
+"""
+Holds the PAR2Set base class
+"""
+
+__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)"
+
+#    Base.py
+#
+#    Copyright (C) 2006,2007  Ira W. Snyder (devel@irasnyder.com)
+#
+#    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.
+#
+#    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
 
 import re
 import os
 import logging
 
+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.
@@ -15,11 +40,12 @@ import logging
 # 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.
 #
-# Assumptions made about each of the run*() functions:
+# Assumptions made in the runVerifyAndRepair(), runExtract() and runDelete() 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.)
+# 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:
 # ==============================================================================
@@ -28,6 +54,7 @@ import logging
 #
 
 class Base (object):
+       """Base class for all PAR2Set types"""
 
        # Instance Variables
        # ==========================================================================
@@ -39,185 +66,160 @@ class Base (object):
        # 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 = get_basename (p2file)
+               self.basename = rsutil.common.get_basename (p2file)
 
                # Find files that match by name only
-               self.name_matched_files = find_name_matches (self.dir, self.basename)
+               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 = find_par2_files (self.name_matched_files)
+               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 = parse_all_par2 (self.dir, self.p2file, self.all_p2files)
+               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 = no_duplicates (self.name_matched_files + self.prot_matched_files)
+               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 \
-                               list_eq (self.name_matched_files, rhs.name_matched_files) and \
-                               list_eq (self.prot_matched_files, rhs.prot_matched_files)
+                               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 which are likely to change after
-               running an operation, usually one which will create new files."""
+               """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."""
 
-               self.name_matched_files = find_name_matches (self.dir, self.basename)
-               self.all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
+               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)
 
        def runVerifyAndRepair (self):
-               PAR2_CMD = config_get_value ('commands', 'par2repair')
-
-               # assemble the command
-               # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
-               command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
+               """Verify and Repair a PAR2Set. This is done using the par2repair command by
+                  default"""
 
-               for f in self.all_p2files:
-                       if f != self.p2file:
-                               command += "\"%s\" " % os.path.split (f)[1]
-
-               # 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
-
-               return SUCCESS
+               rsutil.common.run_command(['par2repair'] + self.all_p2files, self.dir)
 
        def find_deleteable_files (self):
-               DELETE_REGEX = config_get_value ('regular expressions', 'delete_regex')
+               """Find all files which are deletable by using the regular expression from the
+                  configuration file"""
+
+               DELETE_REGEX = rsutil.common.config_get_value ('regular expressions', 'delete_regex')
                dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
 
                return [f for f in self.all_files if dregex.match (f)]
 
        def delete_list_of_files (self, dir, files, interactive=False):
-               # Delete a list of files
+               """Attempt to delete all files given
 
-               assert os.path.isdir (dir)
+                  dir -- the directory where the files live
+                  files -- the filenames themselves
+                  interactive -- prompt before deleteion"""
 
-               done = False
-               valid_y = ['Y', 'YES']
-               valid_n = ['N', 'NO', '']
+               assert os.path.isdir (dir)
 
+               # If we are interactive, decide yes or no
                if interactive:
-                       while not done:
+                       while True:
                                print 'Do you want to delete the following?:'
                                for f in files:
                                        print f
-                               s = raw_input ('Delete [y/N]: ').upper()
 
-                               if s in valid_y + valid_n:
-                                       done = True
+                               s = raw_input('Delete [y/N]: ').upper()
+
+                               # If we got a valid no answer, leave now
+                               if s in ['N', 'NO', '']:
+                                       return
+
+                               # If we got a valid yes answer, delete them
+                               if s in ['Y', 'YES']:
+                                       break
 
-                       if s in valid_n:
-                               return SUCCESS
+                               # Not a good answer, ask again
+                               print 'Invalid response'
 
+               # We got a yes answer (or are non-interactive), delete the files
                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
 
-               return SUCCESS
+                       # Get the full filename
+                       fullname = os.path.join(dir, f)
+
+                       # Delete the file
+                       try:
+                               os.remove(fullname)
+                               logging.debug('Deleting: %s' % fullname)
+                       except OSError:
+                               logging.error('Failed to delete: %s' % fullname)
 
        def runDelete (self):
+               """Run the delete operation and return the result"""
+
                deleteable_files = self.find_deleteable_files ()
                ret = self.delete_list_of_files (self.dir, deleteable_files, \
-                               options_get_value ('interactive'))
+                               rsutil.common.options_get_value ('interactive'))
 
                return ret
 
        def runAll (self):
+               """Run all of the major sections in the class: repair, extraction, and deletion."""
 
                # Repair Stage
-               ret = self.runVerifyAndRepair ()
-
-               if ret != SUCCESS:
-                       logging.critical ('Repair stage failed for: %s' % self.p2file)
-                       return -ECHECK
+               try:
+                       self.runVerifyAndRepair ()
+               except (RuntimeError, OSError):
+                       logging.critical('Repair stage failed for: %s' % self.p2file)
+                       raise
 
-               self.update_matches ()
+               self.update_matches()
 
                # Extraction Stage
-               ret = self.runExtract ()
-
-               if ret != SUCCESS:
-                       logging.critical ('Extraction stage failed for: %s' % self.p2file)
-                       return -EEXTRACT
+               try:
+                       self.runExtract ()
+               except (RuntimeError, OSError):
+                       logging.critical('Extraction stage failed for: %s' % self.p2file)
+                       raise
 
                self.update_matches ()
 
                # Deletion Stage
-               ret = self.runDelete ()
-
-               if ret != SUCCESS:
-                       logging.critical ('Deletion stage failed for: %s' % self.p2file)
-                       return -EDELETE
-
-               logging.info ('Successfully completed: %s' % self.p2file)
-               return SUCCESS
-
-       def safe_create_directory (self, dir):
-               if dir == None:
-                       return SUCCESS
-
-               if os.path.isdir (dir):
-                       return SUCCESS
-
                try:
-                       os.makedirs (dir)
-                       logging.info ('Created directory: %s' % dir)
-               except OSError:
-                       logging.critical ('FAILED to create directory: %s' % dir)
-                       return -ECREATE
-
-               return SUCCESS
-
-       def runExtract (self, todir=None):
-               """Extract all heads of this set"""
-
-               # Extract to the head's dir if we don't care where to extract
-               if todir == None:
-                       todir = self.dir
+                       self.runDelete ()
+               except (RuntimeError, OSError):
+                       logging.critical('Deletion stage failed for: %s' % self.p2file)
+                       raise
 
-               # Create the directory $todir if it doesn't exist
-               ret = self.safe_create_directory (todir)
+               logging.info ('Successfully completed: %s' % self.p2file)
 
-               if ret != SUCCESS:
-                       return -EEXTRACT
+       def runExtract (self):
+               """Extract all heads of this set and return the result"""
 
                # 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)
-
-                       # Check error code
-                       if ret != SUCCESS:
-                               logging.critical ('Failed extracting: %s' % h)
-                               return -EEXTRACT
-
-               return SUCCESS
+                       full_head = rsutil.common.full_abspath (os.path.join (self.dir, h))
+                       self.extraction_function (full_head, self.dir)
 
        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."""
+
                assert False # You MUST override this on a per-type basis
 
        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.
+               """Extract a single head of this PAR2Set's type.
+
+                  file -- the full path to the file to be extracted
+                  todir -- the directory to extract to"""
 
                assert False # You MUST override this on a per-type basis