feat(clew): copy game directory tree

This commit is contained in:
2026-09-07 15:31:33 -04:00
parent dedb0f5913
commit a96d3cdd16

View File

@@ -5,14 +5,10 @@ import logging
import os import os
import shutil import shutil
import sys import sys
from datetime import UTC, datetime from collections import deque
from pathlib import Path from pathlib import Path
from rpycdec.rpa import extract_rpa LOG = logging.getLogger(__name__)
LOGGER = logging.getLogger("clew")
MANAGED_DIRS = ("extracted",)
def parse_args(argv: list[str]) -> argparse.Namespace: def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@@ -55,91 +51,25 @@ def resolve_output_root(output_root: Path) -> Path:
return output_root.resolve() return output_root.resolve()
def prepare_output(output_root: Path, overwrite: bool) -> Path: def get_output_directory(output_root: Path, folder: str) -> Path:
for name in MANAGED_DIRS: directory = output_root / folder
path = output_root / name directory.mkdir(parents=True, exist_ok=True)
if path.exists() and not overwrite: return directory
LOGGER.critical("refusing to overwrite existing output path: %s", path)
raise SystemExit(2)
if overwrite:
for name in MANAGED_DIRS:
shutil.rmtree(output_root / name, ignore_errors=True)
return output_root / "extracted"
def get_folder(path: Path) -> Path: def extract_game_files(game_root: Path, output_path: Path) -> None:
path.mkdir(parents=True, exist_ok=True) queue = deque(game_root.iterdir())
return path
while queue:
item = queue.popleft()
destination = output_path / item.relative_to(game_root)
class LazyFileHandler(logging.FileHandler): if item.is_dir():
def __init__(self, path: Path, encoding: str) -> None: destination.mkdir(parents=True, exist_ok=True)
super().__init__(path, encoding=encoding, delay=True) queue.extend(item.iterdir())
elif item.is_file():
def _open(self): destination.parent.mkdir(parents=True, exist_ok=True)
get_folder(Path(self.baseFilename).parent) shutil.copy2(item, destination)
return super()._open()
def relative_to(root: Path, path: Path) -> Path:
return path.resolve().relative_to(root.resolve())
def normalize_suffixes(suffixes: list[str] | None) -> list[str] | None:
if suffixes is None:
return None
normalized = []
for suffix in suffixes:
if not suffix:
LOGGER.critical("suffix must not be empty")
raise SystemExit(2)
if not suffix.startswith("."):
suffix = f".{suffix}"
normalized.append(suffix)
return normalized
def copy_loose_file(file: Path, game_root: Path, extracted_dir: Path) -> None:
relative = relative_to(game_root, file)
destination = extracted_dir / relative
get_folder(destination.parent)
shutil.copy2(file, destination)
LOGGER.info("copied loose file: %s", relative)
def extract_archive(
archive: Path,
game_root: Path,
extracted_dir: Path,
suffixes: list[str] | None,
) -> None:
archive_relative = relative_to(game_root, archive)
output_dir = get_folder(extracted_dir / archive_relative.with_suffix(""))
LOGGER.info("extracting archive: %s", archive_relative)
with archive.open("rb") as handle:
extract_rpa(handle, output_dir=str(output_dir), suffixes=suffixes)
LOGGER.info("extracted archive: %s", archive_relative)
def configure_logging(output_root: Path) -> None:
log_level = logging.INFO
timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
logs_dir = output_root / "logs"
log_file = logs_dir / f"{logging.getLevelName(log_level)}-clew-{timestamp}.log"
formatter = logging.Formatter("%(message)s")
file_handler = LazyFileHandler(log_file, encoding="utf-8")
file_handler.setFormatter(formatter)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logging.basicConfig(level=log_level, handlers=[file_handler, stream_handler])
def run(args: argparse.Namespace) -> None: def run(args: argparse.Namespace) -> None:
@@ -147,25 +77,20 @@ def run(args: argparse.Namespace) -> None:
output_root = resolve_output_root(args.output_root) output_root = resolve_output_root(args.output_root)
if not game_root.is_dir(): if not game_root.is_dir():
LOGGER.critical("game root is not a directory: %s", game_root) LOG.critical(f'game root is not a directory: {game_root}')
raise SystemExit(2) raise SystemExit(2)
suffixes = normalize_suffixes(args.suffix) extracted = get_output_directory(output_root, "extracted")
extracted_dir = prepare_output(output_root, args.overwrite) extract_game_files(game_root, extracted)
LOGGER.info("copying loose files")
for file in sorted(game_root.rglob("*")):
if file.is_file() and file.suffix.lower() != ".rpa":
copy_loose_file(file, game_root, extracted_dir)
LOGGER.info("extracting archives")
for archive in sorted(game_root.rglob("*.rpa")):
extract_archive(archive, game_root, extracted_dir, suffixes)
LOGGER.info("done")
if __name__ == "__main__": if __name__ == "__main__":
args = parse_args(sys.argv[1:]) args = parse_args(sys.argv[1:])
configure_logging(resolve_output_root(args.output_root))
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] %(levelname)s %(filename)s:%(lineno)d | %(message)s",
datefmt="%Y-%m-%d][%H:%M:%S",
)
run(args) run(args)