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:
@@ -52,6 +52,7 @@ enum UiOperationKind {
|
||||
Installing,
|
||||
Updating,
|
||||
Uninstalling,
|
||||
RemovingDownload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
@@ -230,6 +231,54 @@ async fn uninstall_game(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remove_downloaded_game(
|
||||
id: String,
|
||||
state: tauri::State<'_, LanSpreadState>,
|
||||
) -> tauri::Result<bool> {
|
||||
if state
|
||||
.inner()
|
||||
.active_operations
|
||||
.read()
|
||||
.await
|
||||
.contains_key(&id)
|
||||
{
|
||||
log::warn!("Game already has an active operation: {id}");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let Some((downloaded, installed)) = state
|
||||
.inner()
|
||||
.games
|
||||
.read()
|
||||
.await
|
||||
.get_game_by_id(&id)
|
||||
.map(|game| (game.downloaded, game.installed))
|
||||
else {
|
||||
log::warn!("Ignoring downloaded-file removal for unknown game: {id}");
|
||||
return Ok(false);
|
||||
};
|
||||
if !downloaded || installed {
|
||||
log::warn!(
|
||||
"Ignoring downloaded-file removal for {id}: downloaded={downloaded}, installed={installed}"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let peer_ctrl_arc = state.inner().peer_ctrl.clone();
|
||||
let peer_ctrl = peer_ctrl_arc.read().await.clone();
|
||||
if let Some(peer_ctrl) = peer_ctrl {
|
||||
if let Err(e) = peer_ctrl.send(PeerCommand::RemoveDownloadedGame { id }) {
|
||||
log::error!("Failed to send message to peer: {e:?}");
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
log::warn!("Peer system not initialized yet");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_peer_count(state: tauri::State<'_, LanSpreadState>) -> tauri::Result<usize> {
|
||||
let peer_ctrl_arc = state.inner().peer_ctrl.clone();
|
||||
@@ -505,6 +554,7 @@ fn ui_operation_from_peer(operation: ActiveOperationKind) -> UiOperationKind {
|
||||
ActiveOperationKind::Installing => UiOperationKind::Installing,
|
||||
ActiveOperationKind::Updating => UiOperationKind::Updating,
|
||||
ActiveOperationKind::Uninstalling => UiOperationKind::Uninstalling,
|
||||
ActiveOperationKind::RemovingDownload => UiOperationKind::RemovingDownload,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,6 +1114,33 @@ async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) {
|
||||
"PeerEvent::UninstallGameFailed",
|
||||
);
|
||||
}
|
||||
PeerEvent::RemoveDownloadedGameBegin { id } => {
|
||||
log::info!("PeerEvent::RemoveDownloadedGameBegin received for {id}");
|
||||
emit_game_id_event(
|
||||
app_handle,
|
||||
"game-remove-download-begin",
|
||||
&id,
|
||||
"PeerEvent::RemoveDownloadedGameBegin",
|
||||
);
|
||||
}
|
||||
PeerEvent::RemoveDownloadedGameFinished { id } => {
|
||||
log::info!("PeerEvent::RemoveDownloadedGameFinished received for {id}");
|
||||
emit_game_id_event(
|
||||
app_handle,
|
||||
"game-remove-download-finished",
|
||||
&id,
|
||||
"PeerEvent::RemoveDownloadedGameFinished",
|
||||
);
|
||||
}
|
||||
PeerEvent::RemoveDownloadedGameFailed { id } => {
|
||||
log::warn!("PeerEvent::RemoveDownloadedGameFailed received for {id}");
|
||||
emit_game_id_event(
|
||||
app_handle,
|
||||
"game-remove-download-failed",
|
||||
&id,
|
||||
"PeerEvent::RemoveDownloadedGameFailed",
|
||||
);
|
||||
}
|
||||
PeerEvent::PeerConnected(addr) => {
|
||||
log::info!("Peer connected: {addr}");
|
||||
emit_peer_addr_event(app_handle, "peer-connected", addr);
|
||||
@@ -1315,6 +1392,7 @@ pub fn run() {
|
||||
update_game_directory,
|
||||
update_game,
|
||||
uninstall_game,
|
||||
remove_downloaded_game,
|
||||
get_peer_count,
|
||||
get_game_thumbnail,
|
||||
get_unpack_logs
|
||||
|
||||
Reference in New Issue
Block a user