#!/usr/bin/env python3 """ Migrate flat STORAGE/REPLAYS// dirs → STORAGE/REPLAYS/SRE// Run in dry-run first (default), then with --execute to actually move files. Pass --move to delete each source dir after a verified copy. Usage: python scripts/migrate_replays_to_sre.py python scripts/migrate_replays_to_sre.py --execute python scripts/migrate_replays_to_sre.py --execute --move """ from __future__ import annotations import argparse import os import re import shutil from pathlib import Path _storage_env = os.environ.get("STORAGE_VOL_PATH", "").strip() if not _storage_env: raise RuntimeError("STORAGE_VOL_PATH must be set") STORAGE_DIR = Path(_storage_env) DEFAULT_SOURCE = STORAGE_DIR / "REPLAYS" DEFAULT_DEST = STORAGE_DIR / "REPLAYS" / "SRE" HEX_RE = re.compile(r"^[0-9a-fA-F]+$") def _iter_files(root: Path) -> list[Path]: return [p for p in root.rglob("*") if p.is_file()] def _copy_dir(src: Path, dst: Path, dry_run: bool) -> tuple[int, int]: copied = skipped = 0 for src_file in _iter_files(src): dst_file = dst / src_file.relative_to(src) if dst_file.exists() and dst_file.stat().st_size == src_file.stat().st_size: skipped += 1 continue copied += 1 if not dry_run: dst_file.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src_file, dst_file) return copied, skipped def _fully_copied(src: Path, dst: Path) -> bool: for src_file in _iter_files(src): dst_file = dst / src_file.relative_to(src) if not dst_file.exists() or dst_file.stat().st_size != src_file.stat().st_size: return False return True def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE, help=f"Flat REPLAYS root (default: {DEFAULT_SOURCE})") parser.add_argument("--dest", type=Path, default=DEFAULT_DEST, help=f"REPLAYS/SRE target (default: {DEFAULT_DEST})") parser.add_argument("--execute", action="store_true", help="Actually copy files (default is dry-run).") parser.add_argument("--move", action="store_true", help="Remove each source dir after a verified copy (requires --execute).") args = parser.parse_args() source: Path = args.source.expanduser().resolve() dest: Path = args.dest.expanduser().resolve() dry_run = not args.execute if args.move and dry_run: parser.error("--move requires --execute") if not source.exists(): raise SystemExit(f"Source does not exist: {source}") if source == dest: raise SystemExit("Source and destination are the same directory") print(f"Source : {source}") print(f"Dest : {dest}") print(f"Mode : {'DRY-RUN' if dry_run else 'EXECUTE'}" + (" + MOVE" if args.move else "")) print() dirs_found = dirs_skipped = files_copied = files_skipped = 0 for entry in sorted(source.iterdir()): if not entry.is_dir(): continue # Skip SRE/TSS subdirs if they already exist (idempotent) if entry.name in {"SRE", "TSS"}: continue # Only migrate hex session-ID dirs if not HEX_RE.fullmatch(entry.name): print(f" skip {entry.name} (not a hex session dir)") continue dirs_found += 1 target = dest / entry.name.lower() c, s = _copy_dir(entry, target, dry_run) files_copied += c files_skipped += s print(f" {'[dry] ' if dry_run else ''}{'copy' if c else 'skip'} " f"{entry.name} → SRE/{entry.name.lower()} " f"(copy {c}, already-there {s})") if args.move and not dry_run and _fully_copied(entry, target): shutil.rmtree(entry) print(f" removed source dir: {entry}") print() print(f"Dirs processed : {dirs_found}") print(f"Files copied : {files_copied}") print(f"Files skipped : {files_skipped}") if dry_run: print("\nDry-run only — re-run with --execute to copy.") return 0 if __name__ == "__main__": raise SystemExit(main())