diff --git a/comicarchive.py b/comicarchive.py index f749d44..95bc816 100644 --- a/comicarchive.py +++ b/comicarchive.py @@ -20,67 +20,238 @@ from genericmetadata import GenericMetadata from filenameparser import FileNameParser -# This is a custom function for writing a comment to a zip file, -# since the built-in one doesn't seem to work on Windows and Mac OS/X +class ZipArchiver(): + + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + zf = zipfile.ZipFile( self.path, 'r' ) + comment = zf.comment + zf.close() + return comment -# Fortunately, the zip comment is at the end of the file, and it's -# easy to manipulate. See this website for more info: -# see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure + def setArchiveComment( self, comment ): + writeZipComment( self.path, comment ) -def writeZipComment( filename, comment ): + def readArchiveFile( self, archive_file ): + zf = zipfile.ZipFile( self.path, 'r' ) + data = zf.read( archive_file ) + zf.close() + return data - #get file size - statinfo = os.stat(filename) - file_length = statinfo.st_size + def removeArchiveFile( self, archive_file ): + self.rebuildZipFile( [ archive_file ] ) - fo = open(filename, "r+b") + def writeArchiveFile( self, archive_file, data ): + # At the moment, no other option but to rebuild the whole + # zip archive w/o the indicated file. Very sucky, but maybe + # another solution can be found + self.rebuildZipFile( [ archive_file ] ) + + #now just add the archive file as a new one + zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED ) + zf.writestr( archive_file, data ) + zf.close() - #the starting position, relative to EOF - pos = -4 + def getArchiveFilenameList( self ): + zf = zipfile.ZipFile( self.path, 'r' ) + namelist = zf.namelist() + zf.close() + return namelist + + # zip helper func + def rebuildZipFile( self, exclude_list ): + + # TODO: use tempfile.mkstemp + # this recompresses the zip archive, without the files in the exclude_list + print "Rebuilding zip {0} without {1}".format( self.path, exclude_list ) + zin = zipfile.ZipFile (self.path, 'r') + zout = zipfile.ZipFile ('tmpnew.zip', 'w') + for item in zin.infolist(): + buffer = zin.read(item.filename) + if ( item.filename not in exclude_list ): + zout.writestr(item, buffer) + + #preserve the old comment + zout.comment = zin.comment + + zout.close() + zin.close() + + # replace with the new file + os.remove( self.path ) + os.rename( 'tmpnew.zip', self.path ) - found = False - value = bytearray() - # walk backwards to find the "End of Central Directory" record - while ( not found ) and ( -pos != file_length ): - # seek, relative to EOF - fo.seek( pos, 2) + def writeZipComment( self, filename, comment ): + """ + This is a custom function for writing a comment to a zip file, + since the built-in one doesn't seem to work on Windows and Mac OS/X - value = fo.read( 4 ) + Fortunately, the zip comment is at the end of the file, and it's + easy to manipulate. See this website for more info: + see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure + """ + + #get file size + statinfo = os.stat(filename) + file_length = statinfo.st_size + + fo = open(filename, "r+b") + + #the starting position, relative to EOF + pos = -4 + + found = False + value = bytearray() + + # walk backwards to find the "End of Central Directory" record + while ( not found ) and ( -pos != file_length ): + # seek, relative to EOF + fo.seek( pos, 2) + + value = fo.read( 4 ) + + #look for the end of central directory signature + if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]): + found = True + else: + # not found, step back another byte + pos = pos - 1 + #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) + + if found: + + # now skip forward 20 bytes to the comment length word + pos += 20 + fo.seek( pos, 2) + + # Pack the length of the comment string + format = "H" # one 2-byte integer + comment_length = struct.pack(format, len(comment)) # pack integer in a binary string + + # write out the length + fo.write( comment_length ) + fo.seek( pos+2, 2) + + # write out the comment itself + fo.write( comment ) + fo.truncate() + fo.close() - #look for the end of central directory signature - if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]): - found = True else: - # not found, step back another byte - pos = pos - 1 - #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) - - if found: + raise Exception('Failed to write comment to zip file!') - # now skip forward 20 bytes to the comment length word - pos += 20 - fo.seek( pos, 2) - - # Pack the length of the comment string - format = "H" # one 2-byte integer - comment_length = struct.pack(format, len(comment)) # pack integer in a binary string - - # write out the length - fo.write( comment_length ) - fo.seek( pos+2, 2) - - # write out the comment itself - fo.write( comment ) - fo.truncate() - fo.close() - - else: - raise Exception('Failed to write comment to zip file!') - #------------------------------------------ +# RAR implementation + +class RarArchiver(): + + def __init__( self, path ): + self.path = path + self.rar_exe_path = None + def getArchiveComment( self ): + + rarc = UnRAR2.RarFile( self.path ) + return rarc.comment + def setArchiveComment( self, comment ): + + if self.rar_exe_path is not None: + # write comment to temp file + tmp_fd, tmp_name = tempfile.mkstemp() + f = os.fdopen(tmp_fd, 'w+b') + f.write( comment ) + f.close() + + # use external program to write comment to Rar archive + call([self.rar_exe_path, 'c', '-z' + tmp_name, self.path]) + + os.remove( tmp_name) + + def readArchiveFile( self, archive_file ): + + entries = UnRAR2.RarFile( self.path ).read_files( archive_file ) + + #entries is a list of of tuples: ( rarinfo, filedata) + if (len(entries) == 1): + return entries[0][1] + else: + return "" + + def writeArchiveFile( self, archive_file, data ): + + if self.rar_exe_path is not None: + + tmp_folder = tempfile.mkdtemp() + + tmp_file = os.path.join( tmp_folder, archive_file ) + + f = open(tmp_file, 'w') + f.write( data ) + f.close() + + # use external program to write comment to Rar archive + call([self.rar_exe_path, 'a', '-ep', self.path, tmp_file]) + + os.remove( tmp_file) + os.rmdir( tmp_folder) + + def removeArchiveFile( self, archive_file ): + if self.rar_exe_path is not None: + + # use external program to remove file from Rar archive + call([self.rar_exe_path, 'd', self.path, archive_file]) + + def getArchiveFilenameList( self ): + + rarc = UnRAR2.RarFile( self.path ) + + return [ item.filename for item in rarc.infolist() ] + +#------------------------------------------ +# Folder implementation +class FolderArchiver(): + + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + pass + def setArchiveComment( self, comment ): + pass + def readArchiveFiler( self ): + pass + def writeArchiveFile( self, archive_file, data ): + pass + def removeArchiveFile( self, archive_file ): + pass + def getArchiveFilenameList( self ): + pass + +#------------------------------------------ +# Unknown implementation +class UnknownArchiver(): + + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + return "" + def setArchiveComment( self, comment ): + return + def readArchiveFilen( self ): + return "" + def writeArchiveFile( self, archive_file, data ): + return + def removeArchiveFile( self, archive_file ): + return + def getArchiveFilenameList( self ): + return [] + +#------------------------------------------------------------------ class ComicArchive: class ArchiveType: @@ -89,47 +260,25 @@ class ComicArchive: def __init__( self, path ): self.path = path self.ci_xml_filename = 'ComicInfo.xml' - self.rar_exe_path = None if self.zipTest(): self.archive_type = self.ArchiveType.Zip - self.getArchiveComment = self.getArchiveComment_zip - self.setArchiveComment = self.setArchiveComment_zip - self.readArchiveFile = self.readArchiveFile_zip - self.writeArchiveFile = self.writeArchiveFile_zip - self.removeArchiveFile = self.removeArchiveFile_zip - self.getArchiveFilenameList = self.getArchiveFilenameList_zip + self.archiver = ZipArchiver( self.path ) elif self.rarTest(): self.archive_type = self.ArchiveType.Rar - self.getArchiveComment = self.getArchiveComment_rar - self.setArchiveComment = self.setArchiveComment_rar - self.readArchiveFile = self.readArchiveFile_rar - self.writeArchiveFile = self.writeArchiveFile_rar - self.removeArchiveFile = self.removeArchiveFile_rar - self.getArchiveFilenameList = self.getArchiveFilenameList_rar - + self.archiver = RarArchiver( self.path ) + elif os.path.isdir( self.path ): self.archive_type = self.ArchiveType.Folder - self.getArchiveComment = self.getArchiveComment_folder - self.setArchiveComment = self.setArchiveComment_folder - self.readArchiveFile = self.readArchiveFile_folder - self.writeArchiveFile = self.writeArchiveFile_folder - self.removeArchiveFile = self.removeArchiveFile_folder - self.getArchiveFilenameList = self.getArchiveFilenameList_folder - + self.archiver = FolderArchiver( self.path ) else: self.archive_type = self.ArchiveType.Unknown - self.getArchiveComment = self.getArchiveComment_unknown - self.setArchiveComment = self.setArchiveComment_unknown - self.readArchiveFile = self.readArchiveFile_unknown - self.writeArchiveFile = self.writeArchiveFile_unknown - self.removeArchiveFile = self.removeArchiveFile_unknown - self.getArchiveFilenameList = self.getArchiveFilenameList_unknown - + self.archiver = UnknownArchiver( self.path ) def setExternalRarProgram( self, rar_exe_path ): - self.rar_exe_path = rar_exe_path + if self.isRar(): + self.archiver.rar_exe_path = rar_exe_path def zipTest( self ): return zipfile.is_zipfile( self.path ) @@ -156,7 +305,7 @@ class ComicArchive: if self.archive_type == self.ArchiveType.Unknown : return False - elif self.isRar() and self.rar_exe_path is None: + elif self.isRar() and self.archiver.rar_exe_path is None: return False elif not os.access(self.path, os.W_OK): @@ -171,10 +320,10 @@ class ComicArchive: if ( ( ( ( self.isZip() ) and - ( ext in [ '.zip', '.cbz' ] )) + ( ext.lower() in [ '.zip', '.cbz' ] )) or (( self.isRar() ) and - ( ext in [ '.rar', '.cbr' ] )) + ( ext.lower() in [ '.rar', '.cbr' ] )) or ( self.isFolder() ) ) @@ -223,7 +372,7 @@ class ComicArchive: return None # get the list file names in the archive, and sort - files = self.getArchiveFilenameList() + files = self.archiver.getArchiveFilenameList() files.sort() # find the first image file, assume it's the cover @@ -231,7 +380,7 @@ class ComicArchive: if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ): break - image_data = self.readArchiveFile( name ) + image_data = self.archiver.readArchiveFile( name ) return image_data @@ -239,7 +388,7 @@ class ComicArchive: count = 0 - for item in self.getArchiveFilenameList(): + for item in self.archiver.getArchiveFilenameList(): if ( item[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ): count += 1 @@ -250,7 +399,7 @@ class ComicArchive: if ( not self.hasCBI() ): return GenericMetadata() - cbi_string = self.getArchiveComment() + cbi_string = self.archiver.getArchiveComment() metadata = ComicBookInfo().metadataFromString( cbi_string ) return metadata @@ -258,7 +407,7 @@ class ComicArchive: def writeCBI( self, metadata ): cbi_string = ComicBookInfo().stringFromMetadata( metadata ) - self.setArchiveComment( cbi_string ) + self.archiver.setArchiveComment( cbi_string ) def removeCBI( self ): self.setArchiveComment( "" ) @@ -269,7 +418,7 @@ class ComicArchive: print self.path, "doesn't has ComicInfo.xml data!" return GenericMetadata() - cix_string = self.readArchiveFile( self.ci_xml_filename ) + cix_string = self.archiver.readArchiveFile( self.ci_xml_filename ) metadata = ComicInfoXml().metadataFromString( cix_string ) return metadata @@ -278,16 +427,16 @@ class ComicArchive: if metadata is not None: cix_string = ComicInfoXml().stringFromMetadata( metadata ) - self.writeArchiveFile( self.ci_xml_filename, cix_string ) + self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string ) def removeCIX( self ): - self.removeArchiveFile( self.ci_xml_filename ) + self.archiver.removeArchiveFile( self.ci_xml_filename ) def hasCIX(self): if not self.seemsToBeAComicArchive(): return False - elif self.ci_xml_filename in self.getArchiveFilenameList(): + elif self.ci_xml_filename in self.archiver.getArchiveFilenameList(): return True else: return False @@ -297,7 +446,7 @@ class ComicArchive: if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ): return False - comment = self.getArchiveComment() + comment = self.archiver.getArchiveComment() return ComicBookInfo().validateString( comment ) def metadataFromFilename( self ): @@ -319,167 +468,3 @@ class ComicArchive: metadata.isEmpty = False return metadata - - #--------------- - # Zip implementation - #--------------- - - def getArchiveComment_zip( self ): - zf = zipfile.ZipFile( self.path, 'r' ) - comment = zf.comment - zf.close() - return comment - - def setArchiveComment_zip( self, comment ): - writeZipComment( self.path, comment ) - - def readArchiveFile_zip( self, archive_file ): - zf = zipfile.ZipFile( self.path, 'r' ) - data = zf.read( archive_file ) - zf.close() - return data - - def removeArchiveFile_zip( self, archive_file ): - self.rebuildZipFile( [ archive_file ] ) - - def writeArchiveFile_zip( self, archive_file, data ): - # At the moment, no other option but to rebuild the whole - # zip archive w/o the indicated file. Very sucky, but maybe - # another solution can be found - self.rebuildZipFile( [ archive_file ] ) - - #now just add the archive file as a new one - zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED ) - zf.writestr( archive_file, data ) - zf.close() - - def getArchiveFilenameList_zip( self ): - zf = zipfile.ZipFile( self.path, 'r' ) - namelist = zf.namelist() - zf.close() - return namelist - - # zip helper func - def rebuildZipFile( self, exclude_list ): - - # TODO: use tempfile.mkstemp - # this recompresses the zip archive, without the files in the exclude_list - print "Rebuilding zip {0} without {1}".format( self.path, exclude_list ) - zin = zipfile.ZipFile (self.path, 'r') - zout = zipfile.ZipFile ('tmpnew.zip', 'w') - for item in zin.infolist(): - buffer = zin.read(item.filename) - if ( item.filename not in exclude_list ): - zout.writestr(item, buffer) - - #preserve the old comment - zout.comment = zin.comment - - zout.close() - zin.close() - - # replace with the new file - os.remove( self.path ) - os.rename( 'tmpnew.zip', self.path ) - - #--------------- - # RAR implementation - #--------------- - - def getArchiveComment_rar( self ): - - rarc = UnRAR2.RarFile( self.path ) - return rarc.comment - - def setArchiveComment_rar( self, comment ): - - if self.rar_exe_path is not None: - # write comment to temp file - tmp_fd, tmp_name = tempfile.mkstemp() - f = os.fdopen(tmp_fd, 'w+b') - f.write( comment ) - f.close() - - # use external program to write comment to Rar archive - call([self.rar_exe_path, 'c', '-z' + tmp_name, self.path]) - - os.remove( tmp_name) - - def readArchiveFile_rar( self, archive_file ): - - entries = UnRAR2.RarFile( self.path ).read_files( archive_file ) - - #entries is a list of of tuples: ( rarinfo, filedata) - if (len(entries) == 1): - return entries[0][1] - else: - return "" - - def writeArchiveFile_rar( self, archive_file, data ): - - if self.rar_exe_path is not None: - - tmp_folder = tempfile.mkdtemp() - - tmp_file = os.path.join( tmp_folder, archive_file ) - - f = open(tmp_file, 'w') - f.write( data ) - f.close() - - # use external program to write comment to Rar archive - call([self.rar_exe_path, 'a', '-ep', self.path, tmp_file]) - - os.remove( tmp_file) - os.rmdir( tmp_folder) - - - - def removeArchiveFile_rar( self, archive_file ): - if self.rar_exe_path is not None: - - # use external program to remove file from Rar archive - call([self.rar_exe_path, 'd', self.path, archive_file]) - - - def getArchiveFilenameList_rar( self ): - - rarc = UnRAR2.RarFile( self.path ) - - return [ item.filename for item in rarc.infolist() ] - - - #--------------- - # Folder implementation - #--------------- - - def getArchiveComment_folder( self ): - pass - def setArchiveComment_folder( self, comment ): - pass - def readArchiveFile_folder( self ): - pass - def writeArchiveFile_folder( self, archive_file, data ): - pass - def removeArchiveFile_folder( self, archive_file ): - pass - def getArchiveFilenameList_folder( self ): - pass - - #--------------- - # Unknown implementation - #--------------- - - def getArchiveComment_unknown( self ): - return "" - def setArchiveComment_unknown( self, comment ): - return - def readArchiveFile_unknown( self ): - return "" - def writeArchiveFile_unknown( self, archive_file, data ): - return - def removeArchiveFile_unknown( self, archive_file ): - return - def getArchiveFilenameList_unknown( self ): - return [] - \ No newline at end of file