Skip to content

Commit 0ddf1a2

Browse files
committed
Skip legacy tar file-directory conflicts
1 parent 06007c3 commit 0ddf1a2

2 files changed

Lines changed: 78 additions & 3 deletions

File tree

scripts/streaming_reindex.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,36 @@ def _mark_source_cache_complete(repo_dir: Path, repo: str) -> None:
491491
tmp.replace(sentinel)
492492

493493

494+
def _copy_tar_member_file(src, target: Path, *, repo: str, member_name: str) -> bool:
495+
"""Copy a tar file member, explicitly skipping legacy file/dir conflicts.
496+
497+
Some old source archives contain path-type collisions, for example a
498+
directory and a regular file at the same path. Blocking the whole corpus on
499+
that single path is worse than losing one impossible-to-materialize member,
500+
but the skip must be visible in logs.
501+
"""
502+
if target.exists() and target.is_dir():
503+
print(
504+
f"WARN {repo}: skip tar file {member_name!r}; target path is already "
505+
f"a directory: {target}",
506+
file=sys.stderr,
507+
flush=True,
508+
)
509+
return False
510+
if target.parent.exists() and not target.parent.is_dir():
511+
print(
512+
f"WARN {repo}: skip tar file {member_name!r}; parent path is not a "
513+
f"directory: {target.parent}",
514+
file=sys.stderr,
515+
flush=True,
516+
)
517+
return False
518+
target.parent.mkdir(parents=True, exist_ok=True)
519+
with open(target, "wb") as out:
520+
shutil.copyfileobj(src, out)
521+
return True
522+
523+
494524
def _iter_complete_source_cache(
495525
source_cache_dir: Path,
496526
should_process: Callable[[str], bool],
@@ -613,9 +643,7 @@ def stream_repo_subtrees(
613643
if src is None:
614644
continue
615645
target = cur_dir / within
616-
target.parent.mkdir(parents=True, exist_ok=True)
617-
with open(target, "wb") as out:
618-
shutil.copyfileobj(src, out)
646+
_copy_tar_member_file(src, target, repo=repo, member_name=name)
619647
if cur_repo is not None and active:
620648
assert cur_dir is not None
621649
yield_dir = cur_dir

tests/test_streaming_reindex_run_checked.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
22

3+
import io
34
import sys
5+
import tarfile
6+
import subprocess
47

58

69
def test_run_checked_times_out_fail_loud(tmp_path) -> None:
@@ -156,6 +159,50 @@ def test_stream_repo_dirs_yields_extracted_src_without_tar(tmp_path) -> None:
156159
assert yielded == [("repo", root / "repo" / "_src")]
157160

158161

162+
def test_stream_repo_subtrees_skips_legacy_file_directory_conflict(
163+
tmp_path,
164+
monkeypatch,
165+
capsys,
166+
) -> None:
167+
import streaming_reindex
168+
169+
tar_path = tmp_path / "source.tar"
170+
zst_path = tmp_path / "source.tar.zst"
171+
with tarfile.open(tar_path, "w") as tf:
172+
child_info = tarfile.TarInfo("cpp_all/repo/conflict/child.txt")
173+
child_payload = b"child creates the conflict directory\n"
174+
child_info.size = len(child_payload)
175+
child_info.mode = 0o644
176+
tf.addfile(child_info, io.BytesIO(child_payload))
177+
178+
file_info = tarfile.TarInfo("cpp_all/repo/conflict")
179+
payload = b"this cannot be materialized over a directory\n"
180+
file_info.size = len(payload)
181+
file_info.mode = 0o644
182+
tf.addfile(file_info, io.BytesIO(payload))
183+
184+
good_info = tarfile.TarInfo("cpp_all/repo/good.cpp")
185+
good_payload = b"int good;\n"
186+
good_info.size = len(good_payload)
187+
good_info.mode = 0o644
188+
tf.addfile(good_info, io.BytesIO(good_payload))
189+
190+
subprocess.run(["zstd", "-q", "-f", str(tar_path), "-o", str(zst_path)], check=True)
191+
monkeypatch.setattr(streaming_reindex, "TARBALL", zst_path)
192+
193+
yielded = list(
194+
streaming_reindex.stream_repo_subtrees(
195+
tmp_path / "work",
196+
lambda repo: repo == "repo",
197+
)
198+
)
199+
200+
assert yielded == [("repo", tmp_path / "work" / "repo" / "_src")]
201+
assert (tmp_path / "work" / "repo" / "_src" / "conflict").is_dir()
202+
assert (tmp_path / "work" / "repo" / "_src" / "good.cpp").read_text() == "int good;\n"
203+
assert "skip tar file" in capsys.readouterr().err
204+
205+
159206
def test_populate_source_cache_only_materializes_cache(tmp_path, monkeypatch) -> None:
160207
import streaming_reindex
161208

0 commit comments

Comments
 (0)