extract_all: resolve archive-stem/file name collisions with a _dup destination instead of crashing
This commit is contained in:
+36
-21
@@ -75,10 +75,25 @@ def free_bytes(path: Path) -> int:
|
|||||||
return shutil.disk_usage(path).free
|
return shutil.disk_usage(path).free
|
||||||
|
|
||||||
|
|
||||||
def extract(archive: Path, dest: Path) -> tuple[bool, str]:
|
def extract(archive: Path, dest: Path) -> tuple[bool, str, Path]:
|
||||||
"""7z x into dest. Empty -p avoids interactive prompts (no password)."""
|
"""7z x into dest. Empty -p avoids interactive prompts (no password).
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
cmd = ["7z", "x", "-y", "-p", f"-o{dest}", str(archive)]
|
Returns (ok, error, dest_used): on a name collision (the archive stem
|
||||||
|
exists as a *file*, e.g. 'model.stl.zip' next to 'model.stl') the
|
||||||
|
destination gets a '_dup' suffix instead of crashing.
|
||||||
|
"""
|
||||||
|
actual = dest
|
||||||
|
for _ in range(5):
|
||||||
|
try:
|
||||||
|
actual.mkdir(parents=True, exist_ok=True)
|
||||||
|
break
|
||||||
|
except FileExistsError:
|
||||||
|
actual = dest.with_name(dest.name + "_dup")
|
||||||
|
except (NotADirectoryError, FileExistsError) as exc:
|
||||||
|
return False, f"cannot create destination {actual}: {exc}", dest
|
||||||
|
else:
|
||||||
|
return False, f"cannot create destination {dest} (repeated collisions)", dest
|
||||||
|
cmd = ["7z", "x", "-y", "-p", f"-o{actual}", str(archive)]
|
||||||
try:
|
try:
|
||||||
# 7z prints legacy-encoded inner filenames (non-UTF8 bytes); decode
|
# 7z prints legacy-encoded inner filenames (non-UTF8 bytes); decode
|
||||||
# tolerantly or the whole run crashes (seen with byte 0x87).
|
# tolerantly or the whole run crashes (seen with byte 0x87).
|
||||||
@@ -87,15 +102,15 @@ def extract(archive: Path, dest: Path) -> tuple[bool, str]:
|
|||||||
encoding="utf-8", errors="replace", timeout=EXTRACT_TIMEOUT,
|
encoding="utf-8", errors="replace", timeout=EXTRACT_TIMEOUT,
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return False, f"extraction timed out after {EXTRACT_TIMEOUT}s"
|
return False, f"extraction timed out after {EXTRACT_TIMEOUT}s", actual
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
return False, f"7z failed to run: {exc}"
|
return False, f"7z failed to run: {exc}", actual
|
||||||
if proc.returncode in (0, 1): # 0 ok, 1 warnings (e.g. minor CRC)
|
if proc.returncode in (0, 1): # 0 ok, 1 warnings (e.g. minor CRC)
|
||||||
if proc.returncode == 1:
|
if proc.returncode == 1:
|
||||||
log.warning("7z reported warnings extracting %s", archive.name)
|
log.warning("7z reported warnings extracting %s", archive.name)
|
||||||
return True, ""
|
return True, "", actual
|
||||||
tail = "\n".join((proc.stdout or "").splitlines()[-5:])
|
tail = "\n".join((proc.stdout or "").splitlines()[-5:])
|
||||||
return False, f"7z exit {proc.returncode}: {tail}"
|
return False, f"7z exit {proc.returncode}: {tail}", actual
|
||||||
|
|
||||||
|
|
||||||
def move_to_broken(path: Path, out_root: Path, src_root: Path, note: str) -> Path:
|
def move_to_broken(path: Path, out_root: Path, src_root: Path, note: str) -> Path:
|
||||||
@@ -126,16 +141,16 @@ def expand_nested(dest: Path, out_root: Path, manifest, src_root: Path,
|
|||||||
if free_bytes(out_root) < MIN_FREE_BYTES:
|
if free_bytes(out_root) < MIN_FREE_BYTES:
|
||||||
log.error("Out of disk space - skipping %s (kept in place)", p)
|
log.error("Out of disk space - skipping %s (kept in place)", p)
|
||||||
continue
|
continue
|
||||||
ok, err = extract(p, stem_dir)
|
ok, err, actual = extract(p, stem_dir)
|
||||||
if not ok:
|
if not ok:
|
||||||
log.error("FAILED nested extraction %s: %s", p, err)
|
log.error("FAILED nested extraction %s: %s", p, err)
|
||||||
move_to_broken(p, out_root, out_root, err)
|
move_to_broken(p, out_root, out_root, err)
|
||||||
continue
|
continue
|
||||||
manifest.write(f"{json_line(p.relative_to(out_root), stem_dir)}\n")
|
manifest.write(f"{json_line(p.relative_to(out_root), actual)}\n")
|
||||||
done.add(str(stem_dir.relative_to(out_root)))
|
done.add(str(actual.relative_to(out_root)))
|
||||||
p.unlink() # archive copy consumed
|
p.unlink() # archive copy consumed
|
||||||
done_count += 1
|
done_count += 1
|
||||||
queue.append(stem_dir)
|
queue.append(actual)
|
||||||
return done_count
|
return done_count
|
||||||
|
|
||||||
|
|
||||||
@@ -174,17 +189,17 @@ def process_root(src_root: Path, out_root: Path, manifest, done: set[str]) -> tu
|
|||||||
log.error("Out of disk space - skipping archive %s", f)
|
log.error("Out of disk space - skipping archive %s", f)
|
||||||
failed += 1
|
failed += 1
|
||||||
continue
|
continue
|
||||||
ok, err = extract(f, dest)
|
ok, err, actual = extract(f, dest)
|
||||||
if not ok:
|
if not ok:
|
||||||
log.error("FAILED %s: %s", rel, err)
|
log.error("FAILED %s: %s", rel, err)
|
||||||
move_to_broken(f, out_root, src_root, err)
|
move_to_broken(f, out_root, src_root, err)
|
||||||
failed += 1
|
failed += 1
|
||||||
continue
|
continue
|
||||||
manifest.write(f"{json_line(Path(idx) / rel, dest)}\n")
|
manifest.write(f"{json_line(Path(idx) / rel, actual)}\n")
|
||||||
done.add(str(dest_rel))
|
done.add(str(actual.relative_to(out_root)))
|
||||||
extracted += 1
|
extracted += 1
|
||||||
log.info("Extracted %s -> %s", rel, dest_rel)
|
log.info("Extracted %s -> %s", rel, actual.relative_to(out_root))
|
||||||
expand_nested(dest, out_root, manifest, src_root, done)
|
expand_nested(actual, out_root, manifest, src_root, done)
|
||||||
else:
|
else:
|
||||||
link_dest = out_root / idx / rel
|
link_dest = out_root / idx / rel
|
||||||
if link_dest.exists():
|
if link_dest.exists():
|
||||||
@@ -233,16 +248,16 @@ def sweep(out_root: Path, manifest, done: set[str]) -> int:
|
|||||||
if free_bytes(out_root) < MIN_FREE_BYTES:
|
if free_bytes(out_root) < MIN_FREE_BYTES:
|
||||||
log.error("Out of disk space - sweep skipping %s", p)
|
log.error("Out of disk space - sweep skipping %s", p)
|
||||||
continue
|
continue
|
||||||
ok, err = extract(p, stem_dir)
|
ok, err, actual = extract(p, stem_dir)
|
||||||
if not ok:
|
if not ok:
|
||||||
log.error("FAILED sweep extraction %s: %s", p, err)
|
log.error("FAILED sweep extraction %s: %s", p, err)
|
||||||
move_to_broken(p, out_root, out_root, err)
|
move_to_broken(p, out_root, out_root, err)
|
||||||
continue
|
continue
|
||||||
manifest.write(f"{json_line(p.relative_to(out_root), stem_dir)}\n")
|
manifest.write(f"{json_line(p.relative_to(out_root), actual)}\n")
|
||||||
done.add(str(stem_dir.relative_to(out_root)))
|
done.add(str(actual.relative_to(out_root)))
|
||||||
p.unlink()
|
p.unlink()
|
||||||
swept += 1
|
swept += 1
|
||||||
expand_nested(stem_dir, out_root, manifest, out_root, done)
|
expand_nested(actual, out_root, manifest, out_root, done)
|
||||||
return swept
|
return swept
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user