2 # vim: set ts=4 sts=4 sw=4 textwidth=80:
5 Holds the PAR2Set base class
8 __author__ = "Ira W. Snyder (devel@irasnyder.com)"
9 __copyright__ = "Copyright (c) 2006,2007 Ira W. Snyder (devel@irasnyder.com)"
10 __license__ = "GNU GPL v2 (or, at your option, any later version)"
14 # Copyright (C) 2006,2007 Ira W. Snyder (devel@irasnyder.com)
16 # This program is free software; you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation; either version 2 of the License, or
19 # (at your option) any later version.
21 # This program is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
30 import re, os, logging
31 from subprocess import CalledProcessError
32 from PAR2Set import CompareSet, utils
34 # This is a generic class which handles all of the major operations that a PAR2
35 # set will need to be repaired, extracted, and cleaned up. For most subclasses,
36 # you shouldn't need to do very much.
38 # When the repair(), extract(), and delete() methods run, all of the class
39 # instance variables MUST match the in-filesystem state
41 # To create a new subclass:
42 # 1) Override the detect() method to detect your set type, raise TypeError if
43 # this is not your type
44 # 2) Optionally, override extract() to extract your set
45 # 3) Override any other methods where your set differs from the Base
51 # @cs an instance of CompareSet
52 def __init__(self, cs, options):
55 self.options = options
57 # The directory and parity file
58 self.directory = cs.directory
59 self.parityFile = cs.parityFile
61 # The base name of the parity file (for matching)
62 self.baseName = cs.baseName
64 # Files that match by name only
65 self.similarlyNamedFiles = cs.similarlyNamedFiles
67 # PAR2 files from the files matched by name
68 self.PAR2Files = utils.findMatches(r'^.*\.par2', self.similarlyNamedFiles)
70 # All of the files protected by this set
71 self.protectedFiles = cs.protectedFiles
73 # Create a set of all the combined files
74 self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles)
77 # NOTE: Python calls "down" into derived classes automatically
78 # WARNING: This will raise TypeError if it doesn't match
81 ############################################################################
84 return '%s: %s' % (self.__class__.__name__, self.parityFile)
86 ############################################################################
88 def __eq__(self, rhs):
89 return self.allFiles == rhs.allFiles
91 ############################################################################
99 except (CalledProcessError, OSError):
100 logging.critical('Repair stage failed for: %s' % self)
103 self.updateFilesystemState()
108 except (CalledProcessError, OSError):
109 logging.critical('Extraction stage failed for: %s' % self)
112 self.updateFilesystemState()
117 except (CalledProcessError, OSError):
118 logging.critical('Deletion stage failed for: %s' % self)
121 logging.debug ('Successfully completed: %s' % self)
123 ############################################################################
128 # This is overly simple, but it works great for almost everything
129 utils.runCommand(['par2repair'] + self.PAR2Files, self.directory)
131 ############################################################################
137 ############################################################################
139 def findDeletableFiles(self):
141 regex = r'^.*\.(par2|\d|\d\d\d|rar|r\d\d|zip)$'
142 files = utils.findMatches(regex, self.allFiles)
146 ############################################################################
151 # If we aren't to run delete, don't
152 if self.options.delete == False:
155 # Find all deletable files
156 files = self.findDeletableFiles()
159 # If we are interactive, get the user's response, 'yes' or 'no'
160 if self.options.interactive:
162 print 'Do you want to delete the following?:'
166 s = raw_input('Delete [y/N]: ').upper()
168 # If we got a valid no answer, leave now
169 if s in ['N', 'NO', '']:
172 # If we got a valid yes answer, delete them
173 if s in ['Y', 'YES']:
176 # Not a good answer, ask again
177 print 'Invalid response'
179 # We got a yes answer (or are non-interactive), delete the files
182 # Get the full filename
183 fullname = os.path.join(self.directory, f)
189 logging.error('Failed to delete: %s' % fullname)
192 logging.debug('Deleting: %s' % fullname)
194 ############################################################################
196 # Detect if the given files make up a set of this type
198 # Raise a TypeError if there is no match. This is meant to be called
199 # from the constructor, from which we cannot return a value...
202 # This must be overridden by the subclasses that actually implement
203 # the functionality of rarslave
205 # The original implementation used ONLY the following here:
206 # self.similarlyNamedFiles
207 # self.protectedFiles
210 ############################################################################
212 # Update the class' state to match the current filesystem state
214 # This must be called after any operation which can create or delete files
215 # which are relevant to repair(), extract(), and delete()
216 def updateFilesystemState(self):
218 self.similarlyNamedFiles = utils.findFileNameMatches(self.directory,
220 self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles)
222 ############################################################################