[RARSLAVE] Fix extraction
[rarslave2.git] / rarslave.py
index 7e38816..2e9861a 100644 (file)
@@ -2,6 +2,159 @@
 # 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=None):
+               # Extract all heads of this set
+
+               # Create the directory $todir if it doesn't exist
+               if todir != None and 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:
+                       if todir == None:
+                               # Run in the head's directory
+                               extraction_func (h, os.path.dirname (h))
+                       else:
+                               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 (indir)
+
+       # 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"""
@@ -33,22 +186,233 @@ def find_likely_files (name, dir):
 
        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"""
+
+       if ext[0] != '.':
+               ext = '.' + ext
+
+       ext = re.escape (ext)
+       regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE)
+       return regex.match (f)
+
+def find_extraction_heads (dir, files):
+       """Takes a list of possible files and finds likely heads of
+          extraction."""
+
+       # NOTE: perhaps this should happen AFTER repair is
+       # NOTE: successful. That way all files would already exist
 
+       # According to various sources online:
+       # 1) pre rar-3.0: .rar .r00 .r01 ...
+       # 2) post rar-3.0: .part01.rar .part02.rar 
+       # 3) zip all ver: .zip 
 
+       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):
+                               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):
+                               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):
+                               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
+
+               if done:
+                       for f in prot_files:
+                               extractor.addHead (dir, f)
+               else:
+                       print 'BADNESS'
+
+       # Make sure we found the type
+       assert extractor != None
+
+       return extractor
+
+def is_oldrar (files):
+       for f in files:
+               if has_extension (f, '.r00'):
+                       return True
+
+def is_newrar (files):
+       for f in files:
+               if has_extension (f, '.part01.rar'):
+                       return True
+
+def is_zip (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 ()