Added beginnings of RAR read/write support
git-svn-id: http://comictagger.googlecode.com/svn/trunk@8 6c5673fe-1810-88d6-992b-cd32ca31540c
This commit is contained in:
parent
02ad323329
commit
7519e10858
177
UnRAR2/__init__.py
Normal file
177
UnRAR2/__init__.py
Normal file
@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""
|
||||
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
|
||||
|
||||
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
|
||||
stable and foolproof.
|
||||
Notice that it has INCOMPATIBLE interface.
|
||||
|
||||
It enables reading and unpacking of archives created with the
|
||||
RAR/WinRAR archivers. There is a low-level interface which is very
|
||||
similar to the C interface provided by UnRAR. There is also a
|
||||
higher level interface which makes some common operations easier.
|
||||
"""
|
||||
|
||||
__version__ = '0.99.2'
|
||||
|
||||
try:
|
||||
WindowsError
|
||||
in_windows = True
|
||||
except NameError:
|
||||
in_windows = False
|
||||
|
||||
if in_windows:
|
||||
from windows import RarFileImplementation
|
||||
else:
|
||||
from unix import RarFileImplementation
|
||||
|
||||
|
||||
import fnmatch, time, weakref
|
||||
|
||||
class RarInfo(object):
|
||||
"""Represents a file header in an archive. Don't instantiate directly.
|
||||
Use only to obtain information about file.
|
||||
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.
|
||||
USE METHODS OF RarFile CLASS INSTEAD.
|
||||
|
||||
Properties:
|
||||
index - index of file within the archive
|
||||
filename - name of the file in the archive including path (if any)
|
||||
datetime - file date/time as a struct_time suitable for time.strftime
|
||||
isdir - True if the file is a directory
|
||||
size - size in bytes of the uncompressed file
|
||||
comment - comment associated with the file
|
||||
|
||||
Note - this is not currently intended to be a Python file-like object.
|
||||
"""
|
||||
|
||||
def __init__(self, rarfile, data):
|
||||
self.rarfile = weakref.proxy(rarfile)
|
||||
self.index = data['index']
|
||||
self.filename = data['filename']
|
||||
self.isdir = data['isdir']
|
||||
self.size = data['size']
|
||||
self.datetime = data['datetime']
|
||||
self.comment = data['comment']
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
try :
|
||||
arcName = self.rarfile.archiveName
|
||||
except ReferenceError:
|
||||
arcName = "[ARCHIVE_NO_LONGER_LOADED]"
|
||||
return '<RarInfo "%s" in "%s">' % (self.filename, arcName)
|
||||
|
||||
class RarFile(RarFileImplementation):
|
||||
|
||||
def __init__(self, archiveName, password=None):
|
||||
"""Instantiate the archive.
|
||||
|
||||
archiveName is the name of the RAR file.
|
||||
password is used to decrypt the files in the archive.
|
||||
|
||||
Properties:
|
||||
comment - comment associated with the archive
|
||||
|
||||
>>> print RarFile('test.rar').comment
|
||||
This is a test.
|
||||
"""
|
||||
self.archiveName = archiveName
|
||||
RarFileImplementation.init(self, password)
|
||||
|
||||
def __del__(self):
|
||||
self.destruct()
|
||||
|
||||
def infoiter(self):
|
||||
"""Iterate over all the files in the archive, generating RarInfos.
|
||||
|
||||
>>> import os
|
||||
>>> for fileInArchive in RarFile('test.rar').infoiter():
|
||||
... print os.path.split(fileInArchive.filename)[-1],
|
||||
... print fileInArchive.isdir,
|
||||
... print fileInArchive.size,
|
||||
... print fileInArchive.comment,
|
||||
... print tuple(fileInArchive.datetime)[0:5],
|
||||
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)
|
||||
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59
|
||||
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01
|
||||
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47
|
||||
"""
|
||||
for params in RarFileImplementation.infoiter(self):
|
||||
yield RarInfo(self, params)
|
||||
|
||||
def infolist(self):
|
||||
"""Return a list of RarInfos, descripting the contents of the archive."""
|
||||
return list(self.infoiter())
|
||||
|
||||
def read_files(self, condition='*'):
|
||||
"""Read specific files from archive into memory.
|
||||
If "condition" is a list of numbers, then return files which have those positions in infolist.
|
||||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||
and returns boolean True (extract) or False (skip).
|
||||
If "condition" is omitted, all files are returned.
|
||||
|
||||
Returns list of tuples (RarInfo info, str contents)
|
||||
"""
|
||||
checker = condition2checker(condition)
|
||||
return RarFileImplementation.read_files(self, checker)
|
||||
|
||||
|
||||
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True):
|
||||
"""Extract specific files from archive to disk.
|
||||
|
||||
If "condition" is a list of numbers, then extract files which have those positions in infolist.
|
||||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||
and returns either boolean True (extract) or boolean False (skip).
|
||||
DEPRECATED: If "condition" callback returns string (only supported for Windows) -
|
||||
that string will be used as a new name to save the file under.
|
||||
If "condition" is omitted, all files are extracted.
|
||||
|
||||
"path" is a directory to extract to
|
||||
"withSubpath" flag denotes whether files are extracted with their full path in the archive.
|
||||
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.
|
||||
|
||||
Returns list of RarInfos for extracted files."""
|
||||
checker = condition2checker(condition)
|
||||
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)
|
||||
|
||||
def condition2checker(condition):
|
||||
"""Converts different condition types to callback"""
|
||||
if type(condition) in [str, unicode]:
|
||||
def smatcher(info):
|
||||
return fnmatch.fnmatch(info.filename, condition)
|
||||
return smatcher
|
||||
elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]:
|
||||
def imatcher(info):
|
||||
return info.index in condition
|
||||
return imatcher
|
||||
elif callable(condition):
|
||||
return condition
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
30
UnRAR2/rar_exceptions.py
Normal file
30
UnRAR2/rar_exceptions.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||
|
||||
|
||||
class ArchiveHeaderBroken(Exception): pass
|
||||
class InvalidRARArchive(Exception): pass
|
||||
class FileOpenError(Exception): pass
|
||||
class IncorrectRARPassword(Exception): pass
|
||||
class InvalidRARArchiveUsage(Exception): pass
|
139
UnRAR2/test_UnRAR2.py
Normal file
139
UnRAR2/test_UnRAR2.py
Normal file
@ -0,0 +1,139 @@
|
||||
import os, sys
|
||||
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
|
||||
|
||||
def cleanup(dir='test'):
|
||||
for path, dirs, files in os.walk(dir):
|
||||
for fn in files:
|
||||
os.remove(os.path.join(path, fn))
|
||||
for dir in dirs:
|
||||
os.removedirs(os.path.join(path, dir))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# reuse RarArchive object, en
|
||||
cleanup()
|
||||
rarc = UnRAR2.RarFile('test.rar')
|
||||
rarc.infolist()
|
||||
for info in rarc.infoiter():
|
||||
saveinfo = info
|
||||
assert (str(info)=="""<RarInfo "test" in "test.rar">""")
|
||||
break
|
||||
rarc.extract()
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert os.path.exists('test'+os.sep+'this.py')
|
||||
del rarc
|
||||
assert (str(saveinfo)=="""<RarInfo "test" in "[ARCHIVE_NO_LONGER_LOADED]">""")
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test.rar').extract()
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar matching the wildcard *.txt
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt')
|
||||
assert os.path.exists('test'+os.sep+'test.txt')
|
||||
assert not os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
|
||||
# check the name and size of each file, extracting small ones
|
||||
cleanup()
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
assert archive.comment == 'This is a test.'
|
||||
archive.extract(lambda rarinfo: rarinfo.size <= 1024)
|
||||
for rarinfo in archive.infoiter():
|
||||
if rarinfo.size <= 1024 and not rarinfo.isdir:
|
||||
assert rarinfo.size == os.stat(rarinfo.filename).st_size
|
||||
assert file('test'+os.sep+'test.txt', 'rt').read() == 'This is only a test.'
|
||||
assert not os.path.exists('test'+os.sep+'this.py')
|
||||
cleanup()
|
||||
|
||||
|
||||
# extract this.py, overriding it's destination
|
||||
cleanup('test2')
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
archive.extract('*.py', 'test2', False)
|
||||
assert os.path.exists('test2'+os.sep+'this.py')
|
||||
cleanup('test2')
|
||||
|
||||
|
||||
# extract test.txt to memory
|
||||
cleanup()
|
||||
archive = UnRAR2.RarFile('test.rar')
|
||||
entries = UnRAR2.RarFile('test.rar').read_files('*test.txt')
|
||||
assert len(entries)==1
|
||||
assert entries[0][0].filename.endswith('test.txt')
|
||||
assert entries[0][1]=='This is only a test.'
|
||||
|
||||
|
||||
# extract all the files in test.rar with overwriting
|
||||
cleanup()
|
||||
fo = open('test'+os.sep+'test.txt',"wt")
|
||||
fo.write("blah")
|
||||
fo.close()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt')
|
||||
assert open('test'+os.sep+'test.txt',"rt").read()!="blah"
|
||||
cleanup()
|
||||
|
||||
# extract all the files in test.rar without overwriting
|
||||
cleanup()
|
||||
fo = open('test'+os.sep+'test.txt',"wt")
|
||||
fo.write("blahblah")
|
||||
fo.close()
|
||||
UnRAR2.RarFile('test.rar').extract('*.txt', overwrite = False)
|
||||
assert open('test'+os.sep+'test.txt',"rt").read()=="blahblah"
|
||||
cleanup()
|
||||
|
||||
# list big file in an archive
|
||||
list(UnRAR2.RarFile('test_nulls.rar').infoiter())
|
||||
|
||||
# extract files from an archive with protected files
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test_protected_files.rar', password="protected").extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
||||
try:
|
||||
UnRAR2.RarFile('test_protected_files.rar', password="proteqted").extract()
|
||||
except IncorrectRARPassword:
|
||||
errored = True
|
||||
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
assert errored
|
||||
cleanup()
|
||||
|
||||
# extract files from an archive with protected headers
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test_protected_headers.rar', password="secret").extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
||||
try:
|
||||
UnRAR2.RarFile('test_protected_headers.rar', password="seqret").extract()
|
||||
except IncorrectRARPassword:
|
||||
errored = True
|
||||
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
assert errored
|
||||
cleanup()
|
||||
|
||||
# make sure docstring examples are working
|
||||
import doctest
|
||||
doctest.testmod(UnRAR2)
|
||||
|
||||
# update documentation
|
||||
import pydoc
|
||||
pydoc.writedoc(UnRAR2)
|
||||
|
||||
# cleanup
|
||||
try:
|
||||
os.remove('__init__.pyc')
|
||||
except:
|
||||
pass
|
175
UnRAR2/unix.py
Normal file
175
UnRAR2/unix.py
Normal file
@ -0,0 +1,175 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Unix version uses unrar command line executable
|
||||
|
||||
import subprocess
|
||||
import gc
|
||||
|
||||
import os, os.path
|
||||
import time, re
|
||||
|
||||
from rar_exceptions import *
|
||||
|
||||
class UnpackerNotInstalled(Exception): pass
|
||||
|
||||
rar_executable_cached = None
|
||||
|
||||
def call_unrar(params):
|
||||
"Calls rar/unrar command line executable, returns stdout pipe"
|
||||
global rar_executable_cached
|
||||
if rar_executable_cached is None:
|
||||
for command in ('unrar', 'rar'):
|
||||
try:
|
||||
subprocess.Popen([command], stdout=subprocess.PIPE)
|
||||
rar_executable_cached = command
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
if rar_executable_cached is None:
|
||||
raise UnpackerNotInstalled("No suitable RAR unpacker installed")
|
||||
|
||||
assert type(params) == list, "params must be list"
|
||||
args = [rar_executable_cached] + params
|
||||
try:
|
||||
gc.disable() # See http://bugs.python.org/issue1336
|
||||
return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
finally:
|
||||
gc.enable()
|
||||
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
self.password = password
|
||||
|
||||
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', []).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
raise FileOpenError
|
||||
if line.find("CRC failed")>=0:
|
||||
raise IncorrectRARPassword
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:]
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
else:
|
||||
self.comment = None
|
||||
|
||||
def escaped_password(self):
|
||||
return '-' if self.password == None else self.password
|
||||
|
||||
|
||||
def call(self, cmd, options=[], files=[]):
|
||||
options2 = options + ['p'+self.escaped_password()]
|
||||
soptions = ['-'+x for x in options2]
|
||||
return call_unrar([cmd]+soptions+['--',self.archiveName]+files)
|
||||
|
||||
def infoiter(self):
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', ['c-']).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
raise FileOpenError
|
||||
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not line.startswith('--------------'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
if line.find("CRC failed")>=0:
|
||||
raise IncorrectRARPassword
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
i = 0
|
||||
re_spaces = re.compile(r"\s+")
|
||||
while not line.startswith('--------------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
data = {}
|
||||
data['index'] = i
|
||||
data['filename'] = accum[0].strip()
|
||||
info = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(info[0])
|
||||
attr = info[5]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if checkres==True and not info.isdir:
|
||||
pipe = self.call('p', ['inul'], [info.filename]).stdout
|
||||
res.append((info, pipe.read()))
|
||||
return res
|
||||
|
||||
|
||||
def extract(self, checker, path, withSubpath, overwrite):
|
||||
res = []
|
||||
command = 'x'
|
||||
if not withSubpath:
|
||||
command = 'e'
|
||||
options = []
|
||||
if overwrite:
|
||||
options.append('o+')
|
||||
else:
|
||||
options.append('o-')
|
||||
if not path.endswith(os.sep):
|
||||
path += os.sep
|
||||
names = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if type(checkres) in [str, unicode]:
|
||||
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows")
|
||||
if checkres==True and not info.isdir:
|
||||
names.append(info.filename)
|
||||
res.append(info)
|
||||
names.append(path)
|
||||
proc = self.call(command, options, names)
|
||||
stdoutdata, stderrdata = proc.communicate()
|
||||
if stderrdata.find("CRC failed")>=0:
|
||||
raise IncorrectRARPassword
|
||||
return res
|
||||
|
||||
def destruct(self):
|
||||
pass
|
||||
|
||||
|
309
UnRAR2/windows.py
Normal file
309
UnRAR2/windows.py
Normal file
@ -0,0 +1,309 @@
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||
|
||||
from __future__ import generators
|
||||
|
||||
import ctypes, ctypes.wintypes
|
||||
import os, os.path, sys
|
||||
import Queue
|
||||
import time
|
||||
|
||||
from rar_exceptions import *
|
||||
|
||||
ERAR_END_ARCHIVE = 10
|
||||
ERAR_NO_MEMORY = 11
|
||||
ERAR_BAD_DATA = 12
|
||||
ERAR_BAD_ARCHIVE = 13
|
||||
ERAR_UNKNOWN_FORMAT = 14
|
||||
ERAR_EOPEN = 15
|
||||
ERAR_ECREATE = 16
|
||||
ERAR_ECLOSE = 17
|
||||
ERAR_EREAD = 18
|
||||
ERAR_EWRITE = 19
|
||||
ERAR_SMALL_BUF = 20
|
||||
ERAR_UNKNOWN = 21
|
||||
|
||||
RAR_OM_LIST = 0
|
||||
RAR_OM_EXTRACT = 1
|
||||
|
||||
RAR_SKIP = 0
|
||||
RAR_TEST = 1
|
||||
RAR_EXTRACT = 2
|
||||
|
||||
RAR_VOL_ASK = 0
|
||||
RAR_VOL_NOTIFY = 1
|
||||
|
||||
RAR_DLL_VERSION = 3
|
||||
|
||||
# enum UNRARCALLBACK_MESSAGES
|
||||
UCM_CHANGEVOLUME = 0
|
||||
UCM_PROCESSDATA = 1
|
||||
UCM_NEEDPASSWORD = 2
|
||||
|
||||
architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8
|
||||
dll_name = "unrar.dll"
|
||||
if architecture_bits == 64:
|
||||
dll_name = "x64\\unrar64.dll"
|
||||
|
||||
|
||||
try:
|
||||
unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name))
|
||||
except WindowsError:
|
||||
unrar = ctypes.WinDLL(dll_name)
|
||||
|
||||
|
||||
class RAROpenArchiveDataEx(ctypes.Structure):
|
||||
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
|
||||
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||
ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||
|
||||
_fields_ = [
|
||||
('ArcName', ctypes.c_char_p),
|
||||
('ArcNameW', ctypes.c_wchar_p),
|
||||
('OpenMode', ctypes.c_uint),
|
||||
('OpenResult', ctypes.c_uint),
|
||||
('_CmtBuf', ctypes.c_voidp),
|
||||
('CmtBufSize', ctypes.c_uint),
|
||||
('CmtSize', ctypes.c_uint),
|
||||
('CmtState', ctypes.c_uint),
|
||||
('Flags', ctypes.c_uint),
|
||||
('Reserved', ctypes.c_uint*32),
|
||||
]
|
||||
|
||||
class RARHeaderDataEx(ctypes.Structure):
|
||||
def __init__(self):
|
||||
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||
ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||
|
||||
_fields_ = [
|
||||
('ArcName', ctypes.c_char*1024),
|
||||
('ArcNameW', ctypes.c_wchar*1024),
|
||||
('FileName', ctypes.c_char*1024),
|
||||
('FileNameW', ctypes.c_wchar*1024),
|
||||
('Flags', ctypes.c_uint),
|
||||
('PackSize', ctypes.c_uint),
|
||||
('PackSizeHigh', ctypes.c_uint),
|
||||
('UnpSize', ctypes.c_uint),
|
||||
('UnpSizeHigh', ctypes.c_uint),
|
||||
('HostOS', ctypes.c_uint),
|
||||
('FileCRC', ctypes.c_uint),
|
||||
('FileTime', ctypes.c_uint),
|
||||
('UnpVer', ctypes.c_uint),
|
||||
('Method', ctypes.c_uint),
|
||||
('FileAttr', ctypes.c_uint),
|
||||
('_CmtBuf', ctypes.c_voidp),
|
||||
('CmtBufSize', ctypes.c_uint),
|
||||
('CmtSize', ctypes.c_uint),
|
||||
('CmtState', ctypes.c_uint),
|
||||
('Reserved', ctypes.c_uint*1024),
|
||||
]
|
||||
|
||||
def DosDateTimeToTimeTuple(dosDateTime):
|
||||
"""Convert an MS-DOS format date time to a Python time tuple.
|
||||
"""
|
||||
dosDate = dosDateTime >> 16
|
||||
dosTime = dosDateTime & 0xffff
|
||||
day = dosDate & 0x1f
|
||||
month = (dosDate >> 5) & 0xf
|
||||
year = 1980 + (dosDate >> 9)
|
||||
second = 2*(dosTime & 0x1f)
|
||||
minute = (dosTime >> 5) & 0x3f
|
||||
hour = dosTime >> 11
|
||||
return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1)))
|
||||
|
||||
def _wrap(restype, function, argtypes):
|
||||
result = function
|
||||
result.argtypes = argtypes
|
||||
result.restype = restype
|
||||
return result
|
||||
|
||||
RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, [])
|
||||
|
||||
RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)])
|
||||
|
||||
RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)])
|
||||
|
||||
_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p])
|
||||
def RARSetPassword(*args, **kwargs):
|
||||
_RARSetPassword(*args, **kwargs)
|
||||
|
||||
RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p])
|
||||
|
||||
RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE])
|
||||
|
||||
UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long)
|
||||
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long])
|
||||
|
||||
|
||||
|
||||
RARExceptions = {
|
||||
ERAR_NO_MEMORY : MemoryError,
|
||||
ERAR_BAD_DATA : ArchiveHeaderBroken,
|
||||
ERAR_BAD_ARCHIVE : InvalidRARArchive,
|
||||
ERAR_EOPEN : FileOpenError,
|
||||
}
|
||||
|
||||
class PassiveReader:
|
||||
"""Used for reading files to memory"""
|
||||
def __init__(self, usercallback = None):
|
||||
self.buf = []
|
||||
self.ucb = usercallback
|
||||
|
||||
def _callback(self, msg, UserData, P1, P2):
|
||||
if msg == UCM_PROCESSDATA:
|
||||
data = (ctypes.c_char*P2).from_address(P1).raw
|
||||
if self.ucb!=None:
|
||||
self.ucb(data)
|
||||
else:
|
||||
self.buf.append(data)
|
||||
return 1
|
||||
|
||||
def get_result(self):
|
||||
return ''.join(self.buf)
|
||||
|
||||
class RarInfoIterator(object):
|
||||
def __init__(self, arc):
|
||||
self.arc = arc
|
||||
self.index = 0
|
||||
self.headerData = RARHeaderDataEx()
|
||||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||
if self.res==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
self.arc.lockStatus = "locked"
|
||||
self.arc.needskip = False
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
if self.index>0:
|
||||
if self.arc.needskip:
|
||||
RARProcessFile(self.arc._handle, RAR_SKIP, None, None)
|
||||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||
|
||||
if self.res:
|
||||
raise StopIteration
|
||||
self.arc.needskip = True
|
||||
|
||||
data = {}
|
||||
data['index'] = self.index
|
||||
data['filename'] = self.headerData.FileName
|
||||
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
|
||||
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
|
||||
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
|
||||
if self.headerData.CmtState == 1:
|
||||
data['comment'] = self.headerData.CmtBuf.value
|
||||
else:
|
||||
data['comment'] = None
|
||||
self.index += 1
|
||||
return data
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self.arc.lockStatus = "finished"
|
||||
|
||||
def generate_password_provider(password):
|
||||
def password_provider_callback(msg, UserData, P1, P2):
|
||||
if msg == UCM_NEEDPASSWORD and password!=None:
|
||||
(ctypes.c_char*P2).from_address(P1).value = password
|
||||
return 1
|
||||
return password_provider_callback
|
||||
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
self.password = password
|
||||
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
|
||||
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
|
||||
self.c_callback = UNRARCALLBACK(generate_password_provider(self.password))
|
||||
RARSetCallback(self._handle, self.c_callback, 1)
|
||||
|
||||
if archiveData.OpenResult != 0:
|
||||
raise RARExceptions[archiveData.OpenResult]
|
||||
|
||||
if archiveData.CmtState == 1:
|
||||
self.comment = archiveData.CmtBuf.value
|
||||
else:
|
||||
self.comment = None
|
||||
|
||||
if password:
|
||||
RARSetPassword(self._handle, password)
|
||||
|
||||
self.lockStatus = "ready"
|
||||
|
||||
|
||||
|
||||
def destruct(self):
|
||||
if self._handle and RARCloseArchive:
|
||||
RARCloseArchive(self._handle)
|
||||
|
||||
def make_sure_ready(self):
|
||||
if self.lockStatus == "locked":
|
||||
raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one")
|
||||
if self.lockStatus == "finished":
|
||||
self.destruct()
|
||||
self.init(self.password)
|
||||
|
||||
def infoiter(self):
|
||||
self.make_sure_ready()
|
||||
return RarInfoIterator(self)
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
if checker(info) and not info.isdir:
|
||||
reader = PassiveReader()
|
||||
c_callback = UNRARCALLBACK(reader._callback)
|
||||
RARSetCallback(self._handle, c_callback, 1)
|
||||
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
|
||||
if tmpres==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
self.needskip = False
|
||||
res.append((info, reader.get_result()))
|
||||
return res
|
||||
|
||||
|
||||
def extract(self, checker, path, withSubpath, overwrite):
|
||||
res = []
|
||||
for info in self.infoiter():
|
||||
checkres = checker(info)
|
||||
if checkres!=False and not info.isdir:
|
||||
if checkres==True:
|
||||
fn = info.filename
|
||||
if not withSubpath:
|
||||
fn = os.path.split(fn)[-1]
|
||||
target = os.path.join(path, fn)
|
||||
else:
|
||||
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"
|
||||
target = checkres
|
||||
if overwrite or (not os.path.exists(target)):
|
||||
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
|
||||
if tmpres==ERAR_BAD_DATA:
|
||||
raise IncorrectRARPassword
|
||||
|
||||
self.needskip = False
|
||||
res.append(info)
|
||||
return res
|
||||
|
||||
|
387
comicarchive.py
387
comicarchive.py
@ -5,6 +5,13 @@ A python class to represent a single comic, be it file or folder of images
|
||||
import zipfile
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
from subprocess import call
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".") )
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
|
||||
from options import Options, MetaDataStyle
|
||||
from comicinfoxml import ComicInfoXml
|
||||
@ -76,20 +83,86 @@ def writeZipComment( filename, comment ):
|
||||
|
||||
class ComicArchive:
|
||||
|
||||
class ArchiveType:
|
||||
Zip, Rar, Folder, Unknown = range(4)
|
||||
|
||||
def __init__( self, path ):
|
||||
self.path = path
|
||||
self.ci_xml_filename = 'ComicInfo.xml'
|
||||
|
||||
def isZip( self ):
|
||||
return zipfile.is_zipfile( self.path )
|
||||
|
||||
def isFolder( self ):
|
||||
return False
|
||||
self.rar_exe_path = None
|
||||
|
||||
def isNonWritableArchive( self ):
|
||||
# TODO check for rar, maybe others
|
||||
# also check permissions
|
||||
return False
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
def setExternalRarProgram( self, rar_exe_path ):
|
||||
self.rar_exe_path = rar_exe_path
|
||||
|
||||
def zipTest( self ):
|
||||
return zipfile.is_zipfile( self.path )
|
||||
|
||||
def rarTest( self ):
|
||||
try:
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
except InvalidRARArchive:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def isZip( self ):
|
||||
return self.archive_type == self.ArchiveType.Zip
|
||||
|
||||
def isRar( self ):
|
||||
return self.archive_type == self.ArchiveType.Rar
|
||||
|
||||
def isFolder( self ):
|
||||
return self.archive_type == self.ArchiveType.Folder
|
||||
|
||||
def isWritable( self ):
|
||||
if self.archive_type == self.ArchiveType.Unknown :
|
||||
return False
|
||||
|
||||
elif self.isRar() and self.rar_exe_path is None:
|
||||
return False
|
||||
|
||||
elif not os.access(self.path, os.W_OK):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def seemsToBeAComicArchive( self ):
|
||||
# TODO this will need to be fleshed out to support RAR and Folder
|
||||
@ -97,9 +170,17 @@ class ComicArchive:
|
||||
ext = os.path.splitext(self.path)[1].lower()
|
||||
|
||||
if (
|
||||
( self.isZip() ) and
|
||||
( ext in [ '.zip', '.cbz' ] ) and
|
||||
( self.getNumberOfPages() > 3)
|
||||
( ( ( self.isZip() ) and
|
||||
( ext in [ '.zip', '.cbz' ] ))
|
||||
or
|
||||
(( self.isRar() ) and
|
||||
( ext in [ '.rar', '.cbr' ] ))
|
||||
or
|
||||
( self.isFolder() ) )
|
||||
|
||||
and
|
||||
( self.getNumberOfPages() > 3)
|
||||
|
||||
):
|
||||
return True
|
||||
else:
|
||||
@ -141,10 +222,8 @@ class ComicArchive:
|
||||
if self.getNumberOfPages() == 0:
|
||||
return None
|
||||
|
||||
zf = zipfile.ZipFile (self.path, 'r')
|
||||
|
||||
# get the list file names in the archive, and sort
|
||||
files = zf.namelist()
|
||||
files = self.getArchiveFilenameList()
|
||||
files.sort()
|
||||
|
||||
# find the first image file, assume it's the cover
|
||||
@ -152,8 +231,7 @@ class ComicArchive:
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
|
||||
break
|
||||
|
||||
image_data = zf.read( name )
|
||||
zf.close()
|
||||
image_data = self.readArchiveFile( name )
|
||||
|
||||
return image_data
|
||||
|
||||
@ -161,24 +239,18 @@ class ComicArchive:
|
||||
|
||||
count = 0
|
||||
|
||||
if self.isZip():
|
||||
zf = zipfile.ZipFile (self.path, 'r')
|
||||
for item in zf.infolist():
|
||||
if ( item.filename[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
|
||||
count += 1
|
||||
zf.close()
|
||||
for item in self.getArchiveFilenameList():
|
||||
if ( item[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def readCBI( self ):
|
||||
|
||||
if ( not self.hasCBI() ):
|
||||
print self.path, " isn't a zip or doesn't has CBI data!"
|
||||
return GenericMetadata()
|
||||
|
||||
zf = zipfile.ZipFile( self.path, "r" )
|
||||
cbi_string = zf.comment
|
||||
zf.close()
|
||||
cbi_string = self.getArchiveComment()
|
||||
|
||||
metadata = ComicBookInfo().metadataFromString( cbi_string )
|
||||
return metadata
|
||||
@ -186,101 +258,46 @@ class ComicArchive:
|
||||
def writeCBI( self, metadata ):
|
||||
|
||||
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
|
||||
writeZipComment( self.path, cbi_string )
|
||||
self.setArchiveComment( cbi_string )
|
||||
|
||||
def removeCBI( self ):
|
||||
print "ATB --->removing CBI"
|
||||
writeZipComment( self.path, "" )
|
||||
self.setArchiveComment( "" )
|
||||
|
||||
def readCIX( self ):
|
||||
|
||||
# !!!ATB TODO add support for folders
|
||||
|
||||
if (not self.isZip()) or ( not self.hasCIX()):
|
||||
print self.path, " isn't a zip or doesn't has ComicInfo.xml data!"
|
||||
if not self.hasCIX():
|
||||
print self.path, "doesn't has ComicInfo.xml data!"
|
||||
return GenericMetadata()
|
||||
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
cix_string = zf.read( self.ci_xml_filename )
|
||||
zf.close()
|
||||
|
||||
cix_string = self.readArchiveFile( self.ci_xml_filename )
|
||||
|
||||
metadata = ComicInfoXml().metadataFromString( cix_string )
|
||||
return metadata
|
||||
|
||||
def writeCIX(self, metadata):
|
||||
|
||||
# Passing in None for metadata will remove the CIX file from the archive
|
||||
|
||||
# !!!ATB TODO add support for folders
|
||||
if (not self.isZip()):
|
||||
print self.path, "isn't a zip archive!"
|
||||
return
|
||||
|
||||
if metadata == None:
|
||||
cix_string = ""
|
||||
copy_cix = False
|
||||
else:
|
||||
if metadata is not None:
|
||||
cix_string = ComicInfoXml().stringFromMetadata( metadata )
|
||||
copy_cix = True
|
||||
|
||||
# check if an XML file already exists in archive
|
||||
if not self.hasCIX() and copy_cix:
|
||||
|
||||
#simple case: just add the new archive file
|
||||
zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED )
|
||||
zf.writestr( self.ci_xml_filename, cix_string )
|
||||
zf.close()
|
||||
|
||||
else:
|
||||
# If we need to replace it, well, at the moment, no other option
|
||||
# but to rebuild the whole zip again.
|
||||
# very sucky, but maybe another solution can be found
|
||||
|
||||
print "{0} already exists in {1}. Rebuilding it...".format( self.ci_xml_filename, self.path)
|
||||
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 != self.ci_xml_filename ):
|
||||
zout.writestr(item, buffer)
|
||||
|
||||
# now write out the new xml file, if there is one
|
||||
if copy_cix:
|
||||
zout.writestr( self.ci_xml_filename, cix_string )
|
||||
|
||||
#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 )
|
||||
self.writeArchiveFile( self.ci_xml_filename, cix_string )
|
||||
|
||||
def removeCIX( self ):
|
||||
|
||||
self.writeCIX( None )
|
||||
self.removeArchiveFile( self.ci_xml_filename )
|
||||
|
||||
|
||||
def hasCIX(self):
|
||||
|
||||
has = False
|
||||
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
if self.ci_xml_filename in zf.namelist():
|
||||
has = True
|
||||
zf.close()
|
||||
|
||||
return has
|
||||
if not self.seemsToBeAComicArchive():
|
||||
return False
|
||||
elif self.ci_xml_filename in self.getArchiveFilenameList():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def hasCBI(self):
|
||||
if (not self.isZip() ):
|
||||
|
||||
if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
|
||||
return False
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
comment = zf.comment
|
||||
zf.close()
|
||||
|
||||
|
||||
comment = self.getArchiveComment()
|
||||
return ComicBookInfo().validateString( comment )
|
||||
|
||||
def metadataFromFilename( self ):
|
||||
@ -301,4 +318,168 @@ class ComicArchive:
|
||||
|
||||
metadata.isEmpty = False
|
||||
|
||||
return metadata
|
||||
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 []
|
||||
|
@ -66,15 +66,20 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
|
||||
def updateAppTitle( self ):
|
||||
if self.dirtyFlag:
|
||||
dirty_str = " [modified]"
|
||||
else:
|
||||
dirty_str = ""
|
||||
|
||||
if self.comic_archive is None:
|
||||
self.setWindowTitle( self.appName )
|
||||
else:
|
||||
self.setWindowTitle( self.appName + " - " + self.comic_archive.path + dirty_str)
|
||||
mod_str = ""
|
||||
ro_str = ""
|
||||
|
||||
if self.dirtyFlag:
|
||||
mod_str = " [modified]"
|
||||
|
||||
if not self.comic_archive.isWritable():
|
||||
ro_str = " [read only ]"
|
||||
|
||||
self.setWindowTitle( self.appName + " - " + self.comic_archive.path + mod_str + ro_str)
|
||||
|
||||
def configMenus( self):
|
||||
|
||||
@ -166,6 +171,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
return
|
||||
|
||||
ca = ComicArchive( path )
|
||||
ca.setExternalRarProgram( "/usr/bin/rar" )
|
||||
|
||||
if ca is not None and ca.seemsToBeAComicArchive():
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user