Add Copyright / License information + Documentation
[rarslave2.git] / PAR2Set / Base.py
1 #!/usr/bin/env python
2 # vim: set ts=4 sts=4 sw=4 textwidth=92:
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
31 import os
32 import logging
33
34 import rsutil.common
35
36 # This is a fairly generic class which does all of the major things that a PAR2
37 # set will need to have done to be verified and extracted. For most "normal" types
38 # you won't need to override hardly anything.
39 #
40 # It is ok to override other functions if the need arises, just make sure that you
41 # understand why things are done the way that they are in the original versions.
42 #
43 # Assumptions made in the runVerifyAndRepair(), runExtract() and runDelete() functions:
44 # ==============================================================================
45 # The state of self.name_matched_files, self.prot_matched_files, and self.all_files
46 # will be consistent with the real, in-filesystem state at the time that they are
47 # called. This is the reason that runAll() calls update_matches() after running each
48 # operation that will possibly change the filesystem.
49 #
50 # Required overrides:
51 # ==============================================================================
52 # find_extraction_heads ()
53 # extraction_function ()
54 #
55
56 class Base (object):
57         """Base class for all PAR2Set types"""
58
59         # Instance Variables
60         # ==========================================================================
61         # dir                                   -- The directory this set lives in
62         # p2file                                -- The starting PAR2 file
63         # basename                              -- The basename of the set, guessed from the PAR2 file
64         # all_p2files                   -- All PAR2 files of the set, guessed from the PAR2 file name only
65         # name_matched_files    -- Files in this set, guessed by name only
66         # prot_matched_files    -- Files in this set, guessed by parsing the PAR2 only
67
68         def __init__ (self, dir, p2file):
69                 """Default constructor for all PAR2Set types
70
71                    dir -- a directory
72                    p2file -- a PAR2 file inside the given directory"""
73
74                 assert os.path.isdir (dir)
75                 assert os.path.isfile (os.path.join (dir, p2file))
76
77                 # The real "meat" of the class
78                 self.dir = dir
79                 self.p2file = p2file
80                 self.basename = rsutil.common.get_basename (p2file)
81
82                 # Find files that match by name only
83                 self.name_matched_files = rsutil.common.find_name_matches (self.dir, self.basename)
84
85                 # Find all par2 files for this set using name matches
86                 self.all_p2files = rsutil.common.find_par2_files (self.name_matched_files)
87
88                 # Try to get the protected files for this set
89                 self.prot_matched_files = rsutil.common.parse_all_par2 (self.dir, self.p2file, self.all_p2files)
90
91                 # Setup the all_files combined set (for convenience only)
92                 self.all_files = rsutil.common.no_duplicates (self.name_matched_files + self.prot_matched_files)
93
94         def __eq__ (self, rhs):
95                 """Check for equality between PAR2Set types"""
96
97                 return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \
98                                 rsutil.common.list_eq (self.name_matched_files, rhs.name_matched_files) and \
99                                 rsutil.common.list_eq (self.prot_matched_files, rhs.prot_matched_files)
100
101         def update_matches (self):
102                 """Updates the contents of instance variables with the current in-filesystem state.
103                    This should be run after any operation which can create or delete files."""
104
105                 self.name_matched_files = rsutil.common.find_name_matches (self.dir, self.basename)
106                 self.all_files = rsutil.common.no_duplicates (self.name_matched_files + self.prot_matched_files)
107
108         def runVerifyAndRepair (self):
109                 """Verify and Repair a PAR2Set. This is done using the par2repair command by
110                    default"""
111
112                 PAR2_CMD = rsutil.common.config_get_value ('commands', 'par2repair')
113
114                 # assemble the command
115                 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
116                 command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
117
118                 for f in self.all_p2files:
119                         if f != self.p2file:
120                                 command += "\"%s\" " % os.path.split (f)[1]
121
122                 # run the command
123                 ret = rsutil.common.run_command (command, self.dir)
124
125                 # check the result
126                 if ret != 0:
127                         logging.critical ('PAR2 Check / Repair failed: %s' % self.p2file)
128                         return -rsutil.common.ECHECK
129
130                 return rsutil.common.SUCCESS
131
132         def find_deleteable_files (self):
133                 """Find all files which are deletable by using the regular expression from the
134                    configuration file"""
135
136                 DELETE_REGEX = rsutil.common.config_get_value ('regular expressions', 'delete_regex')
137                 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
138
139                 return [f for f in self.all_files if dregex.match (f)]
140
141         def delete_list_of_files (self, dir, files, interactive=False):
142                 """Attempt to delete all files given
143
144                    dir -- the directory where the files live
145                    files -- the filenames themselves
146                    interactive -- prompt before deleteion"""
147
148                 assert os.path.isdir (dir)
149
150                 done = False
151                 valid_y = ['Y', 'YES']
152                 valid_n = ['N', 'NO', '']
153
154                 if interactive:
155                         while not done:
156                                 print 'Do you want to delete the following?:'
157                                 for f in files:
158                                         print f
159                                 s = raw_input ('Delete [y/N]: ').upper()
160
161                                 if s in valid_y + valid_n:
162                                         done = True
163
164                         if s in valid_n:
165                                 return rsutil.common.SUCCESS
166
167                 for f in files:
168                         try:
169                                 os.remove (os.path.join (dir, f))
170                                 logging.debug ('Deleteing: %s' % os.path.join (dir, f))
171                         except:
172                                 logging.error ('Failed to delete: %s' % os.path.join (dir, f))
173                                 return -rsutil.common.EDELETE
174
175                 return rsutil.common.SUCCESS
176
177         def runDelete (self):
178                 """Run the delete operation and return the result"""
179
180                 deleteable_files = self.find_deleteable_files ()
181                 ret = self.delete_list_of_files (self.dir, deleteable_files, \
182                                 rsutil.common.options_get_value ('interactive'))
183
184                 return ret
185
186         def runAll (self):
187                 """Run all of the major sections in the class: repair, extraction, and deletion."""
188
189                 # Repair Stage
190                 ret = self.runVerifyAndRepair ()
191
192                 if ret != rsutil.common.SUCCESS:
193                         logging.critical ('Repair stage failed for: %s' % self.p2file)
194                         return -rsutil.common.ECHECK
195
196                 self.update_matches ()
197
198                 # Extraction Stage
199                 ret = self.runExtract ()
200
201                 if ret != rsutil.common.SUCCESS:
202                         logging.critical ('Extraction stage failed for: %s' % self.p2file)
203                         return -rsutil.common.EEXTRACT
204
205                 self.update_matches ()
206
207                 # Deletion Stage
208                 ret = self.runDelete ()
209
210                 if ret != rsutil.common.SUCCESS:
211                         logging.critical ('Deletion stage failed for: %s' % self.p2file)
212                         return -rsutil.common.EDELETE
213
214                 logging.info ('Successfully completed: %s' % self.p2file)
215                 return rsutil.common.SUCCESS
216
217         def safe_create_directory (self, dir):
218                 """Safely create a directory, logging the result.
219
220                    dir -- the directory to create (None is ignored)"""
221
222                 if dir == None:
223                         return rsutil.common.SUCCESS
224
225                 if os.path.isdir (dir):
226                         return rsutil.common.SUCCESS
227
228                 try:
229                         os.makedirs (dir)
230                         logging.info ('Created directory: %s' % dir)
231                 except OSError:
232                         logging.critical ('FAILED to create directory: %s' % dir)
233                         return -rsutil.common.ECREATE
234
235                 return rsutil.common.SUCCESS
236
237         def runExtract (self, todir=None):
238                 """Extract all heads of this set and return the result"""
239
240                 # Extract to the head's dir if we don't care where to extract
241                 if todir == None:
242                         todir = self.dir
243
244                 # Create the directory $todir if it doesn't exist
245                 ret = self.safe_create_directory (todir)
246
247                 if ret != rsutil.common.SUCCESS:
248                         return -rsutil.common.EEXTRACT
249
250                 # Call the extraction function on each head
251                 for h in self.find_extraction_heads ():
252                         full_head = rsutil.common.full_abspath (os.path.join (self.dir, h))
253                         ret = self.extraction_function (full_head, todir)
254                         logging.debug ('Extraction Function returned: %d' % ret)
255
256                         # Check error code
257                         if ret != rsutil.common.SUCCESS:
258                                 logging.critical ('Failed extracting: %s' % h)
259                                 return -rsutil.common.EEXTRACT
260
261                 return rsutil.common.SUCCESS
262
263         def find_extraction_heads (self):
264                 """Find all extraction heads associated with this set. This must be
265                    overridden for the associated PAR2Set derived class to work."""
266
267                 assert False # You MUST override this on a per-type basis
268
269         def extraction_function (self, file, todir):
270                 """Extract a single head of this PAR2Set's type.
271
272                    file -- the full path to the file to be extracted
273                    todir -- the directory to extract to"""
274
275                 assert False # You MUST override this on a per-type basis
276