PAR2Set/ZIP: fix output directory
[rarslave2.git] / PAR2Set / Base.py
1 #!/usr/bin/env python
2 # vim: set ts=4 sts=4 sw=4 textwidth=80:
3
4 """
5 Holds the PAR2Set base class
6 """
7
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)"
11
12 #    Base.py
13 #
14 #    Copyright (C) 2006,2007  Ira W. Snyder (devel@irasnyder.com)
15 #
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.
20 #
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.
25 #
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
29
30 import re, os, logging
31 from subprocess import CalledProcessError
32 from PAR2Set import CompareSet, utils
33
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.
37 #
38 # When the repair(), extract(), and delete() methods run, all of the class
39 # instance variables MUST match the in-filesystem state
40 #
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
46 #    implementation
47
48 class Base(object):
49
50     # Constructor
51     # @cs an instance of CompareSet
52     def __init__(self, cs, options):
53
54         # Save the options
55         self.options = options
56
57         # The directory and parity file
58         self.directory = cs.directory
59         self.parityFile = cs.parityFile
60
61         # The base name of the parity file (for matching)
62         self.baseName = cs.baseName
63
64         # Files that match by name only
65         self.similarlyNamedFiles = cs.similarlyNamedFiles
66
67         # PAR2 files from the files matched by name
68         self.PAR2Files = utils.findMatches(r'^.*\.par2', self.similarlyNamedFiles)
69
70         # All of the files protected by this set
71         self.protectedFiles = cs.protectedFiles
72
73         # Create a set of all the combined files
74         self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles)
75
76         # Run the detection
77         # NOTE: Python calls "down" into derived classes automatically
78         # WARNING: This will raise TypeError if it doesn't match
79         self.detect()
80
81     ############################################################################
82
83     def __str__(self):
84         return '%s: %s' % (self.__class__.__name__, self.parityFile)
85
86     ############################################################################
87
88     def __eq__(self, rhs):
89         return self.allFiles == rhs.allFiles
90
91     ############################################################################
92
93     # Run all operations
94     def run(self):
95
96         # Repair Stage
97         try:
98             self.repair()
99         except (CalledProcessError, OSError):
100             logging.critical('Repair stage failed for: %s' % self)
101             raise
102
103         self.updateFilesystemState()
104
105         # Extraction Stage
106         try:
107             self.extract ()
108         except (CalledProcessError, OSError):
109             logging.critical('Extraction stage failed for: %s' % self)
110             raise
111
112         self.updateFilesystemState()
113
114         # Deletion Stage
115         try:
116             self.delete ()
117         except (CalledProcessError, OSError):
118             logging.critical('Deletion stage failed for: %s' % self)
119             raise
120
121         logging.debug ('Successfully completed: %s' % self)
122
123     ############################################################################
124
125     # Run the repair
126     def repair(self):
127
128         # This is overly simple, but it works great for almost everything
129         utils.runCommand(['par2repair'] + self.PAR2Files, self.directory)
130
131     ############################################################################
132
133     # Run the extraction
134     def extract(self):
135         pass
136
137     ############################################################################
138
139     def findDeletableFiles(self):
140
141         regex = r'^.*\.(par2|\d|\d\d\d|rar|r\d\d|zip)$'
142         files = utils.findMatches(regex, self.allFiles)
143
144         return files
145
146     ############################################################################
147
148     # Run the deletion
149     def delete(self):
150
151         # If we aren't to run delete, don't
152         if self.options.delete == False:
153             return
154
155         # Find all deletable files
156         files = self.findDeletableFiles()
157         files.sort()
158
159         # If we are interactive, get the user's response, 'yes' or 'no'
160         if self.options.interactive:
161             while True:
162                 print 'Do you want to delete the following?:'
163                 for f in files:
164                     print f
165
166                 s = raw_input('Delete [y/N]: ').upper()
167
168                 # If we got a valid no answer, leave now
169                 if s in ['N', 'NO', '']:
170                     return
171
172                 # If we got a valid yes answer, delete them
173                 if s in ['Y', 'YES']:
174                     break
175
176                 # Not a good answer, ask again
177                 print 'Invalid response'
178
179         # We got a yes answer (or are non-interactive), delete the files
180         for f in files:
181
182             # Get the full filename
183             fullname = os.path.join(self.directory, f)
184
185             # Delete the file
186             try:
187                 os.remove(fullname)
188             except OSError:
189                 logging.error('Failed to delete: %s' % fullname)
190             else:
191                 print 'rm', fullname
192                 logging.debug('Deleting: %s' % fullname)
193
194     ############################################################################
195
196     # Detect if the given files make up a set of this type
197     #
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...
200     def detect(self):
201
202         # This must be overridden by the subclasses that actually implement
203         # the functionality of rarslave
204
205         # The original implementation used ONLY the following here:
206         # self.similarlyNamedFiles
207         # self.protectedFiles
208         raise TypeError
209
210     ############################################################################
211
212     # Update the class' state to match the current filesystem state
213     #
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):
217
218         self.similarlyNamedFiles = utils.findFileNameMatches(self.directory,
219                                                              self.baseName)
220         self.allFiles = set(self.similarlyNamedFiles + self.protectedFiles)
221
222     ############################################################################
223