diff --git a/tools/extract_all.py b/tools/extract_all.py index eeef697..04108a5 100644 --- a/tools/extract_all.py +++ b/tools/extract_all.py @@ -75,10 +75,25 @@ def free_bytes(path: Path) -> int: return shutil.disk_usage(path).free -def extract(archive: Path, dest: Path) -> tuple[bool, str]: - """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)] +def extract(archive: Path, dest: Path) -> tuple[bool, str, Path]: + """7z x into dest. Empty -p avoids interactive prompts (no password). + + 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: # 7z prints legacy-encoded inner filenames (non-UTF8 bytes); decode # 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, ) 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: - 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 == 1: log.warning("7z reported warnings extracting %s", archive.name) - return True, "" + return True, "", actual 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: @@ -126,16 +141,16 @@ def expand_nested(dest: Path, out_root: Path, manifest, src_root: Path, if free_bytes(out_root) < MIN_FREE_BYTES: log.error("Out of disk space - skipping %s (kept in place)", p) continue - ok, err = extract(p, stem_dir) + ok, err, actual = extract(p, stem_dir) if not ok: log.error("FAILED nested extraction %s: %s", p, err) move_to_broken(p, out_root, out_root, err) continue - manifest.write(f"{json_line(p.relative_to(out_root), stem_dir)}\n") - done.add(str(stem_dir.relative_to(out_root))) + manifest.write(f"{json_line(p.relative_to(out_root), actual)}\n") + done.add(str(actual.relative_to(out_root))) p.unlink() # archive copy consumed done_count += 1 - queue.append(stem_dir) + queue.append(actual) 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) failed += 1 continue - ok, err = extract(f, dest) + ok, err, actual = extract(f, dest) if not ok: log.error("FAILED %s: %s", rel, err) move_to_broken(f, out_root, src_root, err) failed += 1 continue - manifest.write(f"{json_line(Path(idx) / rel, dest)}\n") - done.add(str(dest_rel)) + manifest.write(f"{json_line(Path(idx) / rel, actual)}\n") + done.add(str(actual.relative_to(out_root))) extracted += 1 - log.info("Extracted %s -> %s", rel, dest_rel) - expand_nested(dest, out_root, manifest, src_root, done) + log.info("Extracted %s -> %s", rel, actual.relative_to(out_root)) + expand_nested(actual, out_root, manifest, src_root, done) else: link_dest = out_root / idx / rel 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: log.error("Out of disk space - sweep skipping %s", p) continue - ok, err = extract(p, stem_dir) + ok, err, actual = extract(p, stem_dir) if not ok: log.error("FAILED sweep extraction %s: %s", p, err) move_to_broken(p, out_root, out_root, err) continue - manifest.write(f"{json_line(p.relative_to(out_root), stem_dir)}\n") - done.add(str(stem_dir.relative_to(out_root))) + manifest.write(f"{json_line(p.relative_to(out_root), actual)}\n") + done.add(str(actual.relative_to(out_root))) p.unlink() swept += 1 - expand_nested(stem_dir, out_root, manifest, out_root, done) + expand_nested(actual, out_root, manifest, out_root, done) return swept