012c01a960a0b3e01905397bc061751a6f88914d
[rarslave2.git] / PAR2Set / Base.py
1 #!/usr/bin/env python
2 # vim: set ts=4 sts=4 sw=4 textwidth=92:
3
4 from RarslaveCommon import *
5 import Par2Parser
6
7 import re
8 import os
9 import logging
10
11 # This is a fairly generic class which does all of the major things that a PAR2
12 # set will need to have done to be verified and extracted. For most "normal" types
13 # you won't need to override hardly anything.
14 #
15 # It is ok to override other functions if the need arises, just make sure that you
16 # understand why things are done the way that they are in the original versions.
17 #
18 # Assumptions made about each of the run*() functions:
19 # ==============================================================================
20 # The state of self.name_matched_files and self.prot_matched_files will be consistent
21 # with the real, in-filesystem state at the time that they are called.
22 # (This is why runAll() calls update_matches() all the time.)
23 #
24 # Required overrides:
25 # ==============================================================================
26 # find_extraction_heads ()
27 # extraction_function ()
28 #
29
30 class Base (object):
31
32         # Instance Variables
33         # ==========================================================================
34         # dir                                   -- The directory this set lives in
35         # p2file                                -- The starting PAR2 file
36         # basename                              -- The basename of the set, guessed from the PAR2 file
37         # all_p2files                   -- All PAR2 files of the set, guessed from the PAR2 file name only
38         # name_matched_files    -- Files in this set, guessed by name only
39         # prot_matched_files    -- Files in this set, guessed by parsing the PAR2 only
40
41         def __init__ (self, dir, p2file):
42                 assert os.path.isdir (dir)
43                 assert os.path.isfile (os.path.join (dir, p2file))
44
45                 # The real "meat" of the class
46                 self.dir = dir
47                 self.p2file = p2file
48                 self.basename = get_basename (p2file)
49
50                 # Find files that match by name only
51                 self.name_matched_files = find_name_matches (self.dir, self.basename)
52
53                 # Find all par2 files for this set using name matches
54                 self.all_p2files = find_par2_files (self.name_matched_files)
55
56                 # Try to get the protected files for this set
57                 self.prot_matched_files = parse_all_par2 (self.dir, self.p2file, self.all_p2files)
58
59                 # Setup the all_files combined set (for convenience only)
60                 self.all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
61
62         def __eq__ (self, rhs):
63                 return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \
64                                 list_eq (self.name_matched_files, rhs.name_matched_files) and \
65                                 list_eq (self.prot_matched_files, rhs.prot_matched_files)
66
67         def update_matches (self):
68                 """Updates the contents of instance variables which are likely to change after
69                 running an operation, usually one which will create new files."""
70
71                 self.name_matched_files = find_name_matches (self.dir, self.basename)
72                 self.all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
73
74         def runVerifyAndRepair (self):
75                 PAR2_CMD = config_get_value ('commands', 'par2repair')
76
77                 # assemble the command
78                 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
79                 command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
80
81                 for f in self.all_p2files:
82                         if f != self.p2file:
83                                 command += "\"%s\" " % os.path.split (f)[1]
84
85                 # run the command
86                 ret = run_command (command, self.dir)
87
88                 # check the result
89                 if ret != 0:
90                         logging.critical ('PAR2 Check / Repair failed: %s' % self.p2file)
91                         return -ECHECK
92
93                 return SUCCESS
94
95         def find_deleteable_files (self):
96                 DELETE_REGEX = config_get_value ('regular expressions', 'delete_regex')
97                 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
98
99                 return [f for f in self.all_files if dregex.match (f)]
100
101         def delete_list_of_files (self, dir, files, interactive=False):
102                 # Delete a list of files
103
104                 assert os.path.isdir (dir)
105
106                 done = False
107                 valid_y = ['Y', 'YES']
108                 valid_n = ['N', 'NO', '']
109
110                 if interactive:
111                         while not done:
112                                 print 'Do you want to delete the following?:'
113                                 for f in files:
114                                         print f
115                                 s = raw_input ('Delete [y/N]: ').upper()
116
117                                 if s in valid_y + valid_n:
118                                         done = True
119
120                         if s in valid_n:
121                                 return SUCCESS
122
123                 for f in files:
124                         try:
125                                 os.remove (os.path.join (dir, f))
126                                 logging.debug ('Deleteing: %s' % os.path.join (dir, f))
127                         except:
128                                 logging.error ('Failed to delete: %s' % os.path.join (dir, f))
129                                 return -EDELETE
130
131                 return SUCCESS
132
133         def runDelete (self):
134                 deleteable_files = self.find_deleteable_files ()
135                 ret = self.delete_list_of_files (self.dir, deleteable_files, \
136                                 options_get_value ('interactive'))
137
138                 return ret
139
140         def runAll (self):
141
142                 # Repair Stage
143                 ret = self.runVerifyAndRepair ()
144
145                 if ret != SUCCESS:
146                         logging.critical ('Repair stage failed for: %s' % self.p2file)
147                         return -ECHECK
148
149                 self.update_matches ()
150
151                 # Extraction Stage
152                 ret = self.runExtract ()
153
154                 if ret != SUCCESS:
155                         logging.critical ('Extraction stage failed for: %s' % self.p2file)
156                         return -EEXTRACT
157
158                 self.update_matches ()
159
160                 # Deletion Stage
161                 ret = self.runDelete ()
162
163                 if ret != SUCCESS:
164                         logging.critical ('Deletion stage failed for: %s' % self.p2file)
165                         return -EDELETE
166
167                 logging.info ('Successfully completed: %s' % self.p2file)
168                 return SUCCESS
169
170         def safe_create_directory (self, dir):
171                 if dir == None:
172                         return SUCCESS
173
174                 if os.path.isdir (dir):
175                         return SUCCESS
176
177                 try:
178                         os.makedirs (dir)
179                         logging.info ('Created directory: %s' % dir)
180                 except OSError:
181                         logging.critical ('FAILED to create directory: %s' % dir)
182                         return -ECREATE
183
184                 return SUCCESS
185
186         def runExtract (self, todir=None):
187                 """Extract all heads of this set"""
188
189                 # Extract to the head's dir if we don't care where to extract
190                 if todir == None:
191                         todir = self.dir
192
193                 # Create the directory $todir if it doesn't exist
194                 ret = self.safe_create_directory (todir)
195
196                 if ret != SUCCESS:
197                         return -EEXTRACT
198
199                 # Call the extraction function on each head
200                 for h in self.find_extraction_heads ():
201                         full_head = full_abspath (os.path.join (self.dir, h))
202                         ret = self.extraction_function (full_head, todir)
203                         logging.debug ('Extraction Function returned: %d' % ret)
204
205                         # Check error code
206                         if ret != SUCCESS:
207                                 logging.critical ('Failed extracting: %s' % h)
208                                 return -EEXTRACT
209
210                 return SUCCESS
211
212         def find_extraction_heads (self):
213                 assert False # You MUST override this on a per-type basis
214
215         def extraction_function (self, file, todir):
216                 # NOTE: Please keep the prototype the same for all overridden functions.
217                 # Doing so will guarantee that your life is made much easier.
218                 #
219                 # Also note that the todir given will always be valid for the current directory
220                 # when the function is called.
221
222                 assert False # You MUST override this on a per-type basis
223