# vim: set ts=4 sts=4 sw=4 textwidth=112 :
import re, os, sys
+import par2parser
+
+# Global Variables
+(TYPE_OLDRAR, TYPE_NEWRAR, TYPE_ZIP, TYPE_NOEXTRACT) = range (4)
+(ECHECK, EEXTRACT, EDELETE) = range(1,4)
+
+class RarslaveExtractor (object):
+
+ def __init__ (self, type):
+ self.type = type
+ self.heads = []
+
+ def addHead (self, dir, head):
+ assert os.path.isdir (dir)
+ # REQUIRES that the dir is valid, but not that the file is valid, so that
+ # we can move a file that doesn't exist yet.
+ # FIXME: probably CAN add this back, since we should be running this AFTER repair.
+ #assert os.path.isfile (os.path.join (dir, head))
+
+ self.heads.append (os.path.join (dir, head))
+
+ def extract (self, todir):
+ # Extract all heads of this set
+
+ # Create the directory $todir if it doesn't exist
+ if not os.path.isdir (todir):
+ # TODO: LOGGER
+ try:
+ os.makedirs (todir)
+ except OSError:
+ # TODO: LOGGER
+ return -EEXTRACT
+
+ # Extract all heads
+ extraction_func = \
+ { TYPE_OLDRAR : self.__extract_rar,
+ TYPE_NEWRAR : self.__extract_rar,
+ TYPE_ZIP : self.__extract_zip,
+ TYPE_NOEXTRACT : self.__extract_noextract }[self.type]
+
+ # Call the extraction function on each head
+ for h in self.heads:
+ extraction_func (h, todir)
+
+ def __extract_rar (self, file, todir):
+ assert os.path.isfile (file)
+ assert os.path.isdir (todir)
+
+ RAR_CMD = 'unrar x -o+ -- '
+
+ cmd = '%s \"%s\"' % (RAR_CMD, file)
+ ret = run_command (cmd, todir)
+
+ # Check error code
+ if ret != 0:
+ return -EEXTRACT
+
+ def __extract_zip (self, file, todir):
+ ZIP_CMD = 'unzip \"%s\" -d \"%s\"'
+
+ cmd = ZIP_CMD % (file, todir)
+ ret = run_command (cmd)
+
+ # Check error code
+ if ret != 0:
+ return -EEXTRACT
+
+ def __extract_noextract (self, file, todir):
+ # Just move this file to the $todir, since no extraction is needed
+ # FIXME: NOTE: mv will fail by itself if you're moving to the same dir!
+ cmd = 'mv \"%s\" \"%s\"' % (file, todir)
+ ret = run_command (cmd)
+
+ # Check error code
+ if ret != 0:
+ return -EEXTRACT
+
+
+
+class RarslaveRepairer (object):
+ # Verify (and repair) the set
+ # Make sure it worked, otherwise clean up and return failure
+
+ def __init__ (self, dir, file, join=False):
+ self.dir = dir # the directory containing the par2 file
+ self.file = file # the par2 file
+ self.join = join # True if the par2 set is 001 002 ...
+
+ assert os.path.isdir (dir)
+ assert os.path.isfile (os.path.join (dir, file))
+
+ def checkAndRepair (self):
+ # Form the command:
+ # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
+ PAR2_CMD = 'par2repair -- '
+
+ # Get set up
+ basename = get_basename (self.file)
+ all_files = find_likely_files (basename, self.dir)
+ all_files.sort ()
+ par2_files = find_par2_files (all_files)
+
+ # assemble the command
+ command = "%s \"%s\" " % (PAR2_CMD, self.file)
+
+ for f in par2_files:
+ if f != self.file:
+ command += "\"%s\" " % get_filename(f)
+
+ if self.join:
+ for f in all_files:
+ if f not in par2_files:
+ command += "\"%s\" " % get_filename(f)
+
+ # run the command
+ ret = run_command (command, self.dir)
+
+ # check the result
+ if ret != 0:
+ # TODO: logger
+ print 'error during checkAndRepair()'
+ return -ECHECK
+
+def run_command (cmd, indir=None):
+ # Runs the specified command-line in the directory given (or, in the current directory
+ # if none is given). It returns the status code given by the application.
+
+ pwd = os.getcwd ()
+
+ if indir != None:
+ assert os.path.isdir (indir) # MUST be a directory!
+ os.chdir (pwd)
+
+ # FIXME: re-enable this after testing
+ print 'RUNNING (%s): %s' % (indir, cmd)
+ return 0
+
+ # return os.system (cmd)
+
+
+def full_abspath (p):
+ return os.path.abspath (os.path.expanduser (p))
+
+def get_filename (f):
+ # TODO: I don't think that we should enforce this...
+ # TODO: ... because I think we should be able to get the filename, regardless
+ # TODO: of whether this is a legit filename RIGHT NOW or not.
+ # assert os.path.isfile (f)
+ return os.path.split (f)[1]
def get_basename (name):
"""Strips most kinds of endings from a filename"""
return [f for f in os.listdir (dir) if regex.match (f)]
+def find_par2_files (files):
+ """Find all par2 files in the list $files"""
+
+ regex = re.compile ('^.*\.par2$', re.IGNORECASE)
+ return [f for f in files if regex.match (f)]
+
def find_all_par2_files (dir):
"""Finds all par2 files in a directory"""
+ # NOTE: does NOT return absolute paths
if not os.path.isdir (os.path.abspath (dir)):
raise ValueError # bad directory given
dir = os.path.abspath (dir)
- regex = re.compile ('^.*\.par2$', re.IGNORECASE)
+ files = os.listdir (dir)
- # Find all files
- return [f for f in os.listdir (dir) if regex.match (f)]
+ return find_par2_files (files)
def has_extension (f, ext):
"""Checks if f has the extension ext"""
regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE)
return regex.match (f)
-def find_extraction_heads (files):
+def find_extraction_heads (dir, files):
"""Takes a list of possible files and finds likely heads of
extraction."""
# 2) post rar-3.0: .part01.rar .part02.rar
# 3) zip all ver: .zip
- heads = []
+ extractor = None
+ p2files = find_par2_files (files)
# Old RAR type, find all files ending in .rar
if is_oldrar (files):
+ extractor = RarslaveExtractor (TYPE_OLDRAR)
regex = re.compile ('^.*\.rar$', re.IGNORECASE)
for f in files:
if regex.match (f):
- heads.append (f)
-
- return heads
+ extractor.addHead (dir, f)
if is_newrar (files):
+ extractor = RarslaveExtractor (TYPE_NEWRAR)
regex = re.compile ('^.*\.part01.rar$', re.IGNORECASE)
for f in files:
if regex.match (f):
- heads.append (f)
-
- return heads
+ extractor.addHead (dir, f)
if is_zip (files):
+ extractor = RarslaveExtractor (TYPE_ZIP)
regex = re.compile ('^.*\.zip$', re.IGNORECASE)
for f in files:
if regex.match (f):
- heads.append (f)
+ extractor.addHead (dir, f)
+
+ if is_noextract (files):
+ # Use the Par2 Parser (from cfv) here to find out what files are protected.
+ # Since these are not being extracted, they will be mv'd to another directory
+ # later.
+ extractor = RarslaveExtractor (TYPE_NOEXTRACT)
+
+ for f in p2files:
+ done = False
+ try:
+ prot_files = par2parser.get_protected_files (dir, f)
+ done = True
+ except: #FIXME: add the actual exceptions
+ print 'ERROR PARSING P2FILE ...', f
+ continue
+
+ if done:
+ break
- return heads
+ if done:
+ for f in prot_files:
+ extractor.addHead (dir, f)
+ else:
+ print 'BADNESS'
- # Not a type we know yet
- raise ValueError
+ # Make sure we found the type
+ assert extractor != None
+ return extractor
def is_oldrar (files):
for f in files:
if has_extension (f, '.zip'):
return True
+def is_noextract (files):
+ # Type that needs no extraction.
+ # TODO: Add others ???
+ for f in files:
+ if has_extension (f, '.001'):
+ return True
+
+def find_deleteable_files (files):
+ # Deleteable types regex should come from the config
+ dfiles = []
+ dregex = re.compile ('^.*\.(par2|\d|\d\d\d|rar|r\d\d|zip)$', re.IGNORECASE)
+
+ return [f for f in files if dregex.match (f)]
+
+def printlist (li):
+ for f in li:
+ print f
+
+class PAR2Set (object):
+
+ dir = None
+ file = None
+ likely_files = []
+
+ def __init__ (self, dir, file):
+ assert os.path.isdir (dir)
+ assert os.path.isfile (os.path.join (dir, file))
+
+ self.dir = dir
+ self.file = file
+
+ basename = get_basename (file)
+ self.likely_files = find_likely_files (basename, dir)
+
+ def __list_eq (self, l1, l2):
+
+ if len(l1) != len(l2):
+ return False
+
+ for e in l1:
+ if e not in l2:
+ return False
+
+ return True
+
+ def __eq__ (self, rhs):
+ return self.__list_eq (self.likely_files, rhs.likely_files)
+
+ def run_all (self):
+ par2files = find_par2_files (self.likely_files)
+ par2head = par2files[0]
+
+ join = is_noextract (self.likely_files)
+
+ # Repair Stage
+ repairer = RarslaveRepairer (self.dir, par2head, join)
+ ret = repairer.checkAndRepair () # FIXME: Check return value
+
+ if ret: # FAILURE
+ return -ECHECK
+
+ # Extraction Stage
+ extractor = find_extraction_heads (self.dir, self.likely_files)
+ ret = extractor.extract ('extract_dir') # FIXME: Get it from the config
+
+ if ret: # FAILURE
+ return -EEXTRACT
+
+ # Deletion Stage
+ deleteable_files = find_deleteable_files (self.likely_files)
+ ret = delete_list (deleteable_files)
+
+ if ret: # FAILURE
+ return -EDELETE
+
+ return 0
+
+def delete_list (files, interactive=False):
+ # Delete a list of files
+ # TODO: Add the ability to confirm deletion, like in the original rarslave
+
+ if interactive:
+ # TODO: prompt here
+ # prompt -> OK_TO_DELETE -> do nothing, fall through
+ # prompt -> NOT_OK -> return immediately
+ pass
+
+ for f in files:
+ # FIXME: re-enable this in production
+ # os.remove (f)
+ print 'rm', f
+
+ return 0
+
+
+def generate_all_parsets (dir):
+ # Generate all parsets in the given directory.
+
+ assert os.path.isdir (dir) # Directory MUST be valid
+
+ parsets = []
+ p2files = find_all_par2_files (dir)
+
+ for f in p2files:
+ p = PAR2Set (dir, f)
+ if p not in parsets:
+ parsets.append (p)
+
+ return parsets
def main ():
- print find_all_par2_files ('/home/irasnyd/downloads/test_material/01/')
+ TOPDIR = os.path.abspath ('test_material')
+
+ for (dir, subdirs, files) in os.walk (TOPDIR):
+ print 'DEBUG: IN DIRECTORY:', dir
+ parsets = generate_all_parsets (dir)
+ for p in parsets:
+ p.run_all ()
if __name__ == '__main__':
main ()