2 # vim: set ts=4 sts=4 sw=4 textwidth=112 :
7 from RarslaveLogger import RarslaveLogger
10 (TYPE_OLDRAR, TYPE_NEWRAR, TYPE_ZIP, TYPE_NOEXTRACT) = range (4)
11 (SUCCESS, ECHECK, EEXTRACT, EDELETE) = range(4)
12 config = RarslaveConfig.RarslaveConfig()
13 logger = RarslaveLogger ()
15 class RarslaveExtractor (object):
17 def __init__ (self, type):
21 def addHead (self, dir, head):
22 assert os.path.isdir (dir)
23 # REQUIRES that the dir is valid, but not that the file is valid, so that
24 # we can move a file that doesn't exist yet.
25 # FIXME: probably CAN add this back, since we should be running this AFTER repair.
26 #assert os.path.isfile (os.path.join (dir, head))
28 self.heads.append (os.path.join (dir, head))
30 def extract (self, todir=None):
31 # Extract all heads of this set
33 # Create the directory $todir if it doesn't exist
34 if todir != None and not os.path.isdir (todir):
35 logger.addMessage ('Creating directory: %s' % todir, False)
39 logger.addMessage ('FAILED to create directory: %s' % todir)
44 { TYPE_OLDRAR : self.__extract_rar,
45 TYPE_NEWRAR : self.__extract_rar,
46 TYPE_ZIP : self.__extract_zip,
47 TYPE_NOEXTRACT : self.__extract_noextract }[self.type]
49 # Call the extraction function on each head
52 # Run in the head's directory
53 ret = extraction_func (h, os.path.dirname (h))
55 ret = extraction_func (h, todir)
59 logger.addMessage ('Failed extracting: %s' % h)
64 def __extract_rar (self, file, todir):
65 assert os.path.isfile (file)
66 assert os.path.isdir (todir)
68 RAR_CMD = config.get_value ('commands', 'unrar')
70 cmd = '%s \"%s\"' % (RAR_CMD, file)
71 ret = run_command (cmd, todir)
79 def __extract_zip (self, file, todir):
80 ZIP_CMD = config.get_value ('commands', 'unzip')
82 cmd = ZIP_CMD % (file, todir)
83 ret = run_command (cmd)
91 def __extract_noextract (self, file, todir):
92 # Just move this file to the $todir, since no extraction is needed
93 # FIXME: NOTE: mv will fail by itself if you're moving to the same dir!
94 NOEXTRACT_CMD = config.get_value ('commands', 'noextract')
96 cmd = NOEXTRACT_CMD % (file, todir)
97 ret = run_command (cmd)
107 class RarslaveRepairer (object):
108 # Verify (and repair) the set
109 # Make sure it worked, otherwise clean up and return failure
111 def __init__ (self, dir, file, join=False):
112 self.dir = dir # the directory containing the par2 file
113 self.file = file # the par2 file
114 self.join = join # True if the par2 set is 001 002 ...
116 assert os.path.isdir (dir)
117 assert os.path.isfile (os.path.join (dir, file))
119 def checkAndRepair (self):
121 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
122 PAR2_CMD = config.get_value ('commands', 'par2repair')
125 basename = get_basename (self.file)
126 all_files = find_likely_files (basename, self.dir)
128 par2_files = find_par2_files (all_files)
130 # assemble the command
131 command = "%s \"%s\" " % (PAR2_CMD, self.file)
135 command += "\"%s\" " % os.path.split (f)[1]
139 if f not in par2_files:
140 command += "\"%s\" " % os.path.split (f)[1]
143 ret = run_command (command, self.dir)
147 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.file)
152 def run_command (cmd, indir=None):
153 # Runs the specified command-line in the directory given (or, in the current directory
154 # if none is given). It returns the status code given by the application.
159 assert os.path.isdir (indir) # MUST be a directory!
162 # FIXME: re-enable this after testing
163 print 'RUNNING (%s): %s' % (indir, cmd)
166 # return os.system (cmd)
169 def full_abspath (p):
170 return os.path.abspath (os.path.expanduser (p))
172 def get_basename (name):
173 """Strips most kinds of endings from a filename"""
175 regex = config.get_value ('regular expressions', 'basename_regex')
176 r = re.compile (regex, re.IGNORECASE)
183 g = r.match (name).groups()
189 def find_likely_files (name, dir):
190 """Finds files which are likely to be part of the set corresponding
191 to $name in the directory $dir"""
193 if not os.path.isdir (os.path.abspath (dir)):
194 raise ValueError # bad directory given
196 dir = os.path.abspath (dir)
197 ename = re.escape (name)
198 regex = re.compile ('^%s.*$' % (ename, ))
200 return [f for f in os.listdir (dir) if regex.match (f)]
202 def find_par2_files (files):
203 """Find all par2 files in the list $files"""
205 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
206 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
207 return [f for f in files if regex.match (f)]
209 def find_all_par2_files (dir):
210 """Finds all par2 files in a directory"""
211 # NOTE: does NOT return absolute paths
213 if not os.path.isdir (os.path.abspath (dir)):
214 raise ValueError # bad directory given
216 dir = os.path.abspath (dir)
217 files = os.listdir (dir)
219 return find_par2_files (files)
221 def has_extension (f, ext):
222 """Checks if f has the extension ext"""
227 ext = re.escape (ext)
228 regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE)
229 return regex.match (f)
231 def find_extraction_heads (dir, files):
232 """Takes a list of possible files and finds likely heads of
235 # NOTE: perhaps this should happen AFTER repair is
236 # NOTE: successful. That way all files would already exist
238 # According to various sources online:
239 # 1) pre rar-3.0: .rar .r00 .r01 ...
240 # 2) post rar-3.0: .part01.rar .part02.rar
241 # 3) zip all ver: .zip
244 p2files = find_par2_files (files)
246 # Old RAR type, find all files ending in .rar
247 if is_oldrar (files):
248 extractor = RarslaveExtractor (TYPE_OLDRAR)
249 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
252 extractor.addHead (dir, f)
254 if is_newrar (files):
255 extractor = RarslaveExtractor (TYPE_NEWRAR)
256 regex = re.compile ('^.*\.part01.rar$', re.IGNORECASE)
259 extractor.addHead (dir, f)
262 extractor = RarslaveExtractor (TYPE_ZIP)
263 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
266 extractor.addHead (dir, f)
268 if is_noextract (files):
269 # Use the Par2 Parser (from cfv) here to find out what files are protected.
270 # Since these are not being extracted, they will be mv'd to another directory
272 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
277 prot_files = par2parser.get_protected_files (dir, f)
279 except: #FIXME: add the actual exceptions
280 logger.addMessage ('Error parsing PAR2 file: %s', f)
288 extractor.addHead (dir, f)
290 logger.addMessage ('Error parsing all PAR2 files in this set ...', True)
292 # Make sure we found the type
293 if extractor == None:
294 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0])
296 # No-heads here, but it's better than failing completely
297 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
301 def is_oldrar (files):
303 if has_extension (f, '.r00'):
308 def is_newrar (files):
310 if has_extension (f, '.part01.rar'):
317 if has_extension (f, '.zip'):
322 def is_noextract (files):
323 # Type that needs no extraction.
324 # TODO: Add others ???
326 if has_extension (f, '.001'):
331 def find_deleteable_files (files):
332 # Deleteable types regex should come from the config
334 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
335 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
337 return [f for f in files if dregex.match (f)]
343 class PAR2Set (object):
349 def __init__ (self, dir, file):
350 assert os.path.isdir (dir)
351 assert os.path.isfile (os.path.join (dir, file))
356 basename = get_basename (file)
357 self.likely_files = find_likely_files (basename, dir)
359 def __list_eq (self, l1, l2):
361 if len(l1) != len(l2):
370 def __eq__ (self, rhs):
371 return self.__list_eq (self.likely_files, rhs.likely_files)
374 par2files = find_par2_files (self.likely_files)
375 par2head = par2files[0]
377 join = is_noextract (self.likely_files)
380 repairer = RarslaveRepairer (self.dir, par2head, join)
381 ret = repairer.checkAndRepair ()
384 logger.addMessage ('Repair stage failed for: %s' % par2head)
388 EXTRACT_DIR = config.get_value ('directories', 'extract_directory')
389 extractor = find_extraction_heads (self.dir, self.likely_files)
390 ret = extractor.extract (EXTRACT_DIR)
393 logger.addMessage ('Extraction stage failed for: %s' % par2head)
397 DELETE_INTERACTIVE = config.get_value ('options', 'interactive')
398 deleteable_files = find_deleteable_files (self.likely_files)
399 ret = delete_list (deleteable_files, DELETE_INTERACTIVE)
402 logger.addMessage ('Deletion stage failed for: %s' % par2head)
405 logger.addMessage ('Successfully completed: %s' % par2head, True)
408 def delete_list (files, interactive=False):
409 # Delete a list of files
412 valid_y = ['Y', 'YES']
413 valid_n = ['N', 'NO']
417 print 'Do you want to delete the following?:'
418 s = raw_input ('Delete [y/N]: ').upper()
420 if s in valid_y + valid_n:
427 # FIXME: re-enable this in production
429 print 'rm \"%s\"' % f
434 def generate_all_parsets (dir):
435 # Generate all parsets in the given directory.
437 assert os.path.isdir (dir) # Directory MUST be valid
440 p2files = find_all_par2_files (dir)
450 TOPDIR = os.path.abspath ('test_material')
452 for (dir, subdirs, files) in os.walk (TOPDIR):
453 parsets = generate_all_parsets (dir)
457 print '\nRARSLAVE STATUS\n'
458 logger.printAllMessages ()
460 if __name__ == '__main__':