From 8760c6375958a1db647c76859d71cb5f031c60f7 Mon Sep 17 00:00:00 2001 From: NotSoToothless <67082114+FURRO404@users.noreply.github.com> Date: Mon, 25 May 2026 21:24:56 -0700 Subject: [PATCH] update tss and sre replay area (#1269) --- BOT/task_executors.py | 2 +- BOT/utils.py | 2 +- scripts/migrate_replays_to_sre.py | 122 ++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 scripts/migrate_replays_to_sre.py diff --git a/BOT/task_executors.py b/BOT/task_executors.py index 9ba00dc..88b4e0d 100644 --- a/BOT/task_executors.py +++ b/BOT/task_executors.py @@ -283,7 +283,7 @@ async def cleanup_WL() -> None: async def cleanup_replays(): """ - Cleans up replay directories in STORAGE/REPLAYS/: + Cleans up replay directories in STORAGE/REPLAYS/SRE/: - After 12 hours: deletes regenerable files (PNGs, MP4s) - After 48 hours: deletes entire directory (replay JSON included) diff --git a/BOT/utils.py b/BOT/utils.py index 6d25df0..866557f 100644 --- a/BOT/utils.py +++ b/BOT/utils.py @@ -68,7 +68,7 @@ ICONS_DIR = SHARED_DIR / "ICONS" CACHE_DIR = STORAGE_DIR / "CACHE" AUTH_DIR = STORAGE_DIR / "AUTH" STACKS_DIR = STORAGE_DIR / "STACKS" -REPLAYS_DIR = STORAGE_DIR / "REPLAYS" +REPLAYS_DIR = STORAGE_DIR / "REPLAYS" / "SRE" STORAGE_DIR.mkdir(parents=True, exist_ok=True) # Databases diff --git a/scripts/migrate_replays_to_sre.py b/scripts/migrate_replays_to_sre.py new file mode 100644 index 0000000..7d32bb1 --- /dev/null +++ b/scripts/migrate_replays_to_sre.py @@ -0,0 +1,122 @@ +#!/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())