#!/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)
+ except OSError:
+ logging.error('Failed to delete: %s' % fullname)
+ else:
+ print 'rm', fullname
+ logging.debug('Deleting: %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
+ ############################################################################