Use exceptions for error handling
[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                 rsutil.common.run_command(['par2repair'] + self.all_p2files, self.dir)
113
114         def find_deleteable_files (self):
115                 """Find all files which are deletable by using the regular expression from the
116                    configuration file"""
117
118                 DELETE_REGEX = rsutil.common.config_get_value ('regular expressions', 'delete_regex')
119                 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
120
121                 return [f for f in self.all_files if dregex.match (f)]
122
123         def delete_list_of_files (self, dir, files, interactive=False):
124                 """Attempt to delete all files given
125
126                    dir -- the directory where the files live
127                    files -- the filenames themselves
128                    interactive -- prompt before deleteion"""
129
130                 assert os.path.isdir (dir)
131
132                 # If we are interactive, decide yes or no
133                 if interactive:
134                         while True:
135                                 print 'Do you want to delete the following?:'
136                                 for f in files:
137                                         print f
138
139                                 s = raw_input('Delete [y/N]: ').upper()
140
141                                 # If we got a valid no answer, leave now
142                                 if s in ['N', 'NO', '']:
143                                         return
144
145                                 # If we got a valid yes answer, delete them
146                                 if s in ['Y', 'YES']:
147                                         break
148
149                                 # Not a good answer, ask again
150                                 print 'Invalid response'
151
152                 # We got a yes answer (or are non-interactive), delete the files
153                 for f in files:
154
155                         # Get the full filename
156                         fullname = os.path.join(dir, f)
157
158                         # Delete the file
159                         try:
160                                 os.remove(fullname)
161                                 logging.debug('Deleting: %s' % fullname)
162                         except OSError:
163                                 logging.error('Failed to delete: %s' % fullname)
164
165         def runDelete (self):
166                 """Run the delete operation and return the result"""
167
168                 deleteable_files = self.find_deleteable_files ()
169                 ret = self.delete_list_of_files (self.dir, deleteable_files, \
170                                 rsutil.common.options_get_value ('interactive'))
171
172                 return ret
173
174         def runAll (self):
175                 """Run all of the major sections in the class: repair, extraction, and deletion."""
176
177                 # Repair Stage
178                 try:
179                         self.runVerifyAndRepair ()
180                 except (RuntimeError, OSError):
181                         logging.critical('Repair stage failed for: %s' % self.p2file)
182                         raise
183
184                 self.update_matches()
185
186                 # Extraction Stage
187                 try:
188                         self.runExtract ()
189                 except (RuntimeError, OSError):
190                         logging.critical('Extraction stage failed for: %s' % self.p2file)
191                         raise
192
193                 self.update_matches ()
194
195                 # Deletion Stage
196                 try:
197                         self.runDelete ()
198                 except (RuntimeError, OSError):
199                         logging.critical('Deletion stage failed for: %s' % self.p2file)
200                         raise
201
202                 logging.info ('Successfully completed: %s' % self.p2file)
203
204         def runExtract (self):
205                 """Extract all heads of this set and return the result"""
206
207                 # Call the extraction function on each head
208                 for h in self.find_extraction_heads ():
209                         full_head = rsutil.common.full_abspath (os.path.join (self.dir, h))
210                         self.extraction_function (full_head, self.dir)
211
212         def find_extraction_heads (self):
213                 """Find all extraction heads associated with this set. This must be
214                    overridden for the associated PAR2Set derived class to work."""
215
216                 assert False # You MUST override this on a per-type basis
217
218         def extraction_function (self, file, todir):
219                 """Extract a single head of this PAR2Set's type.
220
221                    file -- the full path to the file to be extracted
222                    todir -- the directory to extract to"""
223
224                 assert False # You MUST override this on a per-type basis
225