feat(peer): remove downloaded game files safely

Downloaded but uninstalled games can still occupy significant disk space. Add a
separate removal path for that state instead of overloading uninstall, which is
reserved for deleting only `local/` installs.

The peer runtime now exposes `RemoveDownloadedGame` with matching lifecycle
and active-operation events. The filesystem delete is intentionally strict: the
id must be a catalog game and a single path component, the target must be a
direct child of the configured game directory, the root must not be a symlink,
it must have a regular root-level `version.ini`, and it must not contain
`local/`, `.local.installing/`, or `.local.backup/`. Only then do we recursively
remove the game root.

The Tauri bridge exposes this as `remove_downloaded_game`, the frontend shows a
matching danger action only for downloaded-but-uninstalled games, and a
confirmation dialog warns that re-downloading can take a long time.

Test Plan:
- git diff --check
- just fmt
- RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just test
- RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just clippy
- RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just build

Refs: user redesign nitpick about removing downloaded uninstalled games
This commit is contained in:
2026-05-19 21:00:44 +02:00
parent 74d9266723
commit 62ceb063ac
18 changed files with 628 additions and 31 deletions
@@ -14,12 +14,14 @@ const IN_PROGRESS_INSTALL_STATUSES = new Set<InstallStatus>([
InstallStatus.Downloading,
InstallStatus.Installing,
InstallStatus.Uninstalling,
InstallStatus.Removing,
]);
const RECONCILED_OPERATION_STATUSES = new Set<InstallStatus>([
InstallStatus.Downloading,
InstallStatus.Installing,
InstallStatus.Uninstalling,
InstallStatus.Removing,
]);
export const isInProgress = (status: InstallStatus): boolean =>
@@ -37,6 +39,8 @@ export const installStatusFromActiveOperation = (op: ActiveOperationKind): Insta
return InstallStatus.Installing;
case ActiveOperationKind.Uninstalling:
return InstallStatus.Uninstalling;
case ActiveOperationKind.RemovingDownload:
return InstallStatus.Removing;
}
};
@@ -136,6 +140,8 @@ export const inProgressLabel = (status: InstallStatus): string | undefined => {
return 'Installing…';
case InstallStatus.Uninstalling:
return 'Uninstalling…';
case InstallStatus.Removing:
return 'Removing…';
default:
return undefined;
}