Compare commits
2 Commits
3f6facf45b
...
2bec6d7ad5
Author | SHA1 | Date | |
---|---|---|---|
2bec6d7ad5 | |||
1a574b661a |
@ -9,102 +9,13 @@ import zipfile
|
||||
from typing import cast
|
||||
|
||||
import chardet
|
||||
from zipremove import ZipFile
|
||||
|
||||
from comicapi.archivers import Archiver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZipFile(zipfile.ZipFile):
|
||||
|
||||
def remove(self, zinfo_or_arcname): # type: ignore
|
||||
"""Remove a member from the archive."""
|
||||
|
||||
if self.mode not in ("w", "x", "a"):
|
||||
raise ValueError("remove() requires mode 'w', 'x', or 'a'")
|
||||
if not self.fp:
|
||||
raise ValueError("Attempt to write to ZIP archive that was already closed")
|
||||
if self._writing: # type: ignore[attr-defined]
|
||||
raise ValueError("Can't write to ZIP archive while an open writing handle exists")
|
||||
|
||||
# Make sure we have an existing info object
|
||||
if isinstance(zinfo_or_arcname, zipfile.ZipInfo):
|
||||
zinfo = zinfo_or_arcname
|
||||
# make sure zinfo exists
|
||||
if zinfo not in self.filelist:
|
||||
raise KeyError("There is no item %r in the archive" % zinfo_or_arcname)
|
||||
else:
|
||||
# get the info object
|
||||
zinfo = self.getinfo(zinfo_or_arcname)
|
||||
|
||||
return self._remove_members({zinfo})
|
||||
|
||||
def _remove_members(self, members, *, remove_physical=True, chunk_size=2**20): # type: ignore
|
||||
"""Remove members in a zip file.
|
||||
All members (as zinfo) should exist in the zip; otherwise the zip file
|
||||
will erroneously end in an inconsistent state.
|
||||
"""
|
||||
fp = self.fp
|
||||
assert fp
|
||||
entry_offset = 0
|
||||
member_seen = False
|
||||
|
||||
# get a sorted filelist by header offset, in case the dir order
|
||||
# doesn't match the actual entry order
|
||||
filelist = sorted(self.filelist, key=lambda x: x.header_offset)
|
||||
for i in range(len(filelist)):
|
||||
info = filelist[i]
|
||||
is_member = info in members
|
||||
|
||||
if not (member_seen or is_member):
|
||||
continue
|
||||
|
||||
# get the total size of the entry
|
||||
try:
|
||||
offset = filelist[i + 1].header_offset
|
||||
except IndexError:
|
||||
offset = self.start_dir
|
||||
entry_size = offset - info.header_offset
|
||||
|
||||
if is_member:
|
||||
member_seen = True
|
||||
entry_offset += entry_size
|
||||
|
||||
# update caches
|
||||
self.filelist.remove(info)
|
||||
try:
|
||||
del self.NameToInfo[info.filename]
|
||||
except KeyError:
|
||||
pass
|
||||
continue
|
||||
|
||||
# update the header and move entry data to the new position
|
||||
if remove_physical:
|
||||
old_header_offset = info.header_offset
|
||||
info.header_offset -= entry_offset
|
||||
read_size = 0
|
||||
while read_size < entry_size:
|
||||
fp.seek(old_header_offset + read_size)
|
||||
data = fp.read(min(entry_size - read_size, chunk_size))
|
||||
fp.seek(info.header_offset + read_size)
|
||||
fp.write(data)
|
||||
fp.flush()
|
||||
read_size += len(data)
|
||||
|
||||
# Avoid missing entry if entries have a duplicated name.
|
||||
# Reverse the order as NameToInfo normally stores the last added one.
|
||||
for info in reversed(self.filelist):
|
||||
self.NameToInfo.setdefault(info.filename, info)
|
||||
|
||||
# update state
|
||||
if remove_physical:
|
||||
self.start_dir -= entry_offset
|
||||
self._didModify = True
|
||||
|
||||
# seek to the start of the central dir
|
||||
fp.seek(self.start_dir)
|
||||
|
||||
|
||||
class ZipArchiver(Archiver):
|
||||
"""ZIP implementation"""
|
||||
|
||||
@ -149,7 +60,7 @@ class ZipArchiver(Archiver):
|
||||
try:
|
||||
with ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
if archive_file in files:
|
||||
zf.remove(archive_file)
|
||||
zf.repack([zf.remove(archive_file)])
|
||||
return True
|
||||
except (zipfile.BadZipfile, OSError) as e:
|
||||
logger.error("Error writing zip archive [%s]: %s :: %s", e, self.path, archive_file)
|
||||
@ -163,7 +74,7 @@ class ZipArchiver(Archiver):
|
||||
# now just add the archive file as a new one
|
||||
with ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
if archive_file in files:
|
||||
zf.remove(archive_file)
|
||||
zf.repack([zf.remove(archive_file)])
|
||||
zf.writestr(archive_file, data)
|
||||
return True
|
||||
except (zipfile.BadZipfile, OSError) as e:
|
||||
|
15
setup.cfg
15
setup.cfg
@ -50,6 +50,8 @@ install_requires =
|
||||
text2digits
|
||||
typing-extensions>=4.3.0
|
||||
wordninja
|
||||
zipremove
|
||||
pyicu
|
||||
python_requires = >=3.9
|
||||
|
||||
[options.packages.find]
|
||||
@ -73,16 +75,16 @@ pyinstaller40 =
|
||||
|
||||
[options.extras_require]
|
||||
7z =
|
||||
py7zr
|
||||
py7zr<1
|
||||
all =
|
||||
PyQt5
|
||||
PyQtWebEngine
|
||||
comicinfoxml==0.4.*
|
||||
comicinfoxml==0.5.*
|
||||
gcd-talker>0.1.0
|
||||
metron-talker>0.1.5
|
||||
pillow-avif-plugin>=1.4.1
|
||||
pillow-jxl-plugin>=1.2.5
|
||||
py7zr
|
||||
py7zr<1
|
||||
rarfile>=4.0
|
||||
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
|
||||
archived_tags =
|
||||
@ -92,13 +94,12 @@ avif =
|
||||
cbr =
|
||||
rarfile>=4.0
|
||||
cix =
|
||||
comicinfoxml==0.4.*
|
||||
comicinfoxml==0.5.*
|
||||
gcd =
|
||||
gcd-talker>0.1.0
|
||||
gui =
|
||||
PyQt5
|
||||
icu =
|
||||
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
|
||||
jxl =
|
||||
pillow-jxl-plugin>=1.2.5
|
||||
metron =
|
||||
@ -106,10 +107,10 @@ metron =
|
||||
pyinstaller =
|
||||
PyQt5
|
||||
PyQtWebEngine
|
||||
comicinfoxml==0.4.*
|
||||
comicinfoxml==0.5.*
|
||||
pillow-avif-plugin>=1.4.1
|
||||
pillow-jxl-plugin>=1.2.5
|
||||
py7zr
|
||||
py7zr<1
|
||||
rarfile>=4.0
|
||||
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
|
||||
qtw =
|
||||
|
@ -28,7 +28,9 @@ def test_os_sorted():
|
||||
"!cover",
|
||||
]
|
||||
|
||||
assert comicapi.utils.os_sorted(page_name_list) == [
|
||||
sorted_list = comicapi.utils.os_sorted(page_name_list)
|
||||
|
||||
assert sorted_list == [
|
||||
"!cover",
|
||||
"!cover.jpg",
|
||||
"!cover.tar.gz",
|
||||
|
Reference in New Issue
Block a user