Add BeatMods parsing and userdata restore

This commit is contained in:
pleb
2026-06-29 10:56:57 -07:00
parent 17bd736e59
commit 1f35f6b078
10 changed files with 448 additions and 22 deletions
+34 -2
View File
@@ -19,7 +19,7 @@ from .planner import create_plan
from .scanner import scan_instance
from .state import load_installed_state
from .updates import check_updates
from .userdata import sync_windows_data_repo
from .userdata import restore_windows_data_repo, sync_windows_data_repo
def _json(data: Any) -> None:
@@ -246,6 +246,16 @@ def build_parser() -> argparse.ArgumentParser:
backup.add_argument("--appdata-path", help="Override Beat Saber Windows AppData path")
backup.add_argument("--no-appdata", action="store_true", help="Only copy UserData")
restore = subcommands.add_parser(
"restore-userdata",
help="Restore UserData and AppData from the backups repo into an instance",
parents=[_common_parent()],
)
restore.add_argument("--instance", required=True)
restore.add_argument("--backup-root", default="../backups/beat-saber", help="Backup directory")
restore.add_argument("--appdata-path", help="Override Beat Saber AppData destination path")
restore.add_argument("--no-appdata", action="store_true", help="Only restore UserData")
return parser
@@ -300,7 +310,8 @@ def _run_menu(inst_roots: list[Path], st_root: Path, input_func: Callable[[str],
("bootstrap-check", "Check BSIPA bootstrap", "Verifies recorded bootstrap state and the latest BSIPA log evidence."),
("plan", "Create install plan", "Writes a dry-run JSON plan for locked plugin files before anything is applied."),
("apply", "Apply a plan by path", "Installs exactly the file changes from a previously generated plan JSON."),
("backup-userdata", "Back up UserData", "Creates a timestamped archive of UserData before risky changes."),
("backup-userdata", "Back up UserData", "Copies UserData and AppData into the adjacent backups repo."),
("restore-userdata", "Restore UserData", "Restores UserData and AppData from the backups repo into an instance."),
("change", "Choose another version", "Returns to the Beat Saber version picker."),
]
@@ -574,6 +585,27 @@ def run(argv: list[str] | None = None) -> int:
print(f" {item['source']} -> {item['destination']}")
return 0
if args.command == "restore-userdata":
instance = get_instance(inst_roots, args.instance)
root = repo_root()
backup_root = Path(args.backup_root).expanduser()
if not backup_root.is_absolute():
backup_root = (root / backup_root).resolve()
result = restore_windows_data_repo(
instance=args.instance,
instance_path=instance.path,
backup_root=backup_root,
appdata_path=Path(args.appdata_path).expanduser() if args.appdata_path else None,
include_appdata=not args.no_appdata,
)
print(f"Backup root: {result['backupRoot']}")
for item in result["restored"]:
print(f"{item['label']}: {item['fileCount']} files")
print(f" {item['source']} -> {item['destination']}")
if item["snapshot"]:
print(f" previous: {item['snapshot']}")
return 0
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 2