Skip to content

Commit 230c363

Browse files
committed
Stream package extraction to bound peak memory usage
1 parent d1a7221 commit 230c363

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

src/manage/install_command.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def validate_package(install, dest, *, delete=True):
166166

167167

168168
def extract_package(package, prefix, calculate_dest=Path, *, on_progress=None, repair=False):
169+
import shutil
169170
import zipfile
170171

171172
LOGGER.debug("Starting extract of %s to %s", package, prefix)
@@ -205,8 +206,8 @@ def _calc(prefix, filename, calculate_dest=calculate_dest):
205206
warn_overwrite.append(dest)
206207
continue
207208
ensure_tree(dest)
208-
with open(dest, "wb") as f:
209-
f.write(zf.read(member))
209+
with zf.open(member) as source, open(dest, "wb") as f:
210+
shutil.copyfileobj(source, f, length=1024 * 1024)
210211
on_progress(100)
211212

212213
if warn_out_of_prefix:

tests/test_install_command.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,48 @@
1010
from manage.logging import LOGGER
1111

1212

13+
@pytest.mark.parametrize("suffix", [".zip", ".nupkg"])
14+
@pytest.mark.parametrize("repair", [False, True])
15+
def test_extract_package_streaming(tmp_path, monkeypatch, suffix, repair):
16+
"""Extract in bounded reads while preserving overwrite and repair behavior."""
17+
import zipfile
18+
19+
package = tmp_path / ("package" + suffix)
20+
prefix = tmp_path / "install"
21+
prefix.mkdir()
22+
existing = prefix / "existing.txt"
23+
existing.write_bytes(b"original")
24+
data = bytes(range(256)) * 10000
25+
archive_prefix = "tools/" if suffix == ".nupkg" else ""
26+
with zipfile.ZipFile(package, "w", zipfile.ZIP_DEFLATED) as zf:
27+
zf.writestr(archive_prefix + "nested/data.bin", data)
28+
zf.writestr(archive_prefix + "empty.txt", b"")
29+
zf.writestr(archive_prefix + "existing.txt", b"replacement")
30+
if suffix == ".nupkg":
31+
zf.writestr("metadata.txt", b"ignored")
32+
33+
reads = []
34+
original_read = zipfile.ZipExtFile.read
35+
36+
def bounded_read(self, n=-1):
37+
assert 0 < n <= 1024 * 1024
38+
reads.append(n)
39+
return original_read(self, n)
40+
41+
monkeypatch.setattr(zipfile.ZipExtFile, "read", bounded_read)
42+
progress = []
43+
IC.extract_package(package, prefix, calculate_dest=Path,
44+
on_progress=progress.append, repair=repair)
45+
assert (prefix / "nested/data.bin").read_bytes() == data
46+
assert (prefix / "empty.txt").read_bytes() == b""
47+
assert existing.read_bytes() == (b"replacement" if repair else b"original")
48+
assert not (prefix / "metadata.txt").exists()
49+
assert len(reads) >= 4
50+
assert progress[0] == 0
51+
assert 100 in progress
52+
assert (None in progress) == (not repair)
53+
54+
1355
def test_print_cli_shortcuts(patched_installs, assert_log, monkeypatch, tmp_path):
1456
class Cmd:
1557
scratch = {}

0 commit comments

Comments
 (0)