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
@@ -8,13 +8,14 @@ export interface GameActions {
install: (id: string) => Promise<void>;
update: (id: string) => Promise<void>;
uninstall: (id: string) => Promise<void>;
removeDownload: (id: string) => Promise<void>;
}
/**
* Thin wrappers over the backend `run_game` / `install_game` / `update_game`
* / `uninstall_game` commands. We mark peer-backed downloads as "checking
* peers" and already-downloaded installs as "installing" up-front so the UI
* doesn't have to wait for the first backend event.
* / `uninstall_game` / `remove_downloaded_game` commands. We mark peer-backed
* downloads as "checking peers" and already-downloaded installs as "installing"
* up-front so the UI doesn't have to wait for the first backend event.
*/
export const useGameActions = (games: UseGamesResult): GameActions => {
const play = useCallback(async (id: string) => {
@@ -58,5 +59,13 @@ export const useGameActions = (games: UseGamesResult): GameActions => {
}
}, []);
return { play, install, update, uninstall };
const removeDownload = useCallback(async (id: string) => {
try {
await invoke('remove_downloaded_game', { id });
} catch (err) {
console.error('remove_downloaded_game failed:', err);
}
}, []);
return { play, install, update, uninstall, removeDownload };
};