This commit is contained in:
2025-11-08 18:40:19 +01:00
parent 64b4665b57
commit 5710d87295
2 changed files with 189 additions and 10 deletions
@@ -1,5 +1,5 @@
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
@@ -21,6 +21,8 @@ struct LanSpreadState {
games: Arc<RwLock<GameDB>>,
games_in_download: Arc<RwLock<HashSet<String>>>,
games_folder: Arc<RwLock<String>>,
// Track backup paths for games being updated
game_backups: Arc<RwLock<HashMap<String, PathBuf>>>,
}
#[tauri::command]
@@ -53,6 +55,137 @@ fn install_game(id: String, state: tauri::State<LanSpreadState>) -> bool {
true
}
/// Backup the current game folder by renaming it to `___TO_BE_DELETE___GameNameHere`
/// # Errors
/// Returns error if backup fails
fn backup_game_folder(game_path: &Path) -> eyre::Result<PathBuf> {
if !game_path.exists() {
bail!("Game folder does not exist: {}", game_path.display());
}
let game_name = game_path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("Invalid game folder name"))?;
let parent = game_path
.parent()
.ok_or_else(|| eyre::eyre!("Cannot get parent directory"))?;
let backup_name = format!("___TO_BE_DELETE___{game_name}");
let backup_path = parent.join(backup_name);
// Remove existing backup if it exists
if backup_path.exists() {
std::fs::remove_dir_all(&backup_path)?;
}
// Rename current game folder to backup
std::fs::rename(game_path, &backup_path)?;
log::info!(
"Backed up game folder: {} -> {}",
game_path.display(),
backup_path.display()
);
Ok(backup_path)
}
/// Restore game folder from backup
/// # Errors
/// Returns error if restore fails
fn restore_game_folder(game_path: &Path, backup_path: &Path) -> eyre::Result<()> {
if !backup_path.exists() {
bail!("Backup folder does not exist: {}", backup_path.display());
}
// Remove the partially installed game folder if it exists
if game_path.exists() {
std::fs::remove_dir_all(game_path)?;
}
// Restore backup
std::fs::rename(backup_path, game_path)?;
log::info!(
"Restored game folder from backup: {} -> {}",
backup_path.display(),
game_path.display()
);
Ok(())
}
/// Remove backup folder after successful update
/// # Errors
/// Returns error if cleanup fails
fn cleanup_backup_folder(backup_path: &Path) -> eyre::Result<()> {
if backup_path.exists() {
std::fs::remove_dir_all(backup_path)?;
log::info!("Cleaned up backup folder: {}", backup_path.display());
}
Ok(())
}
#[tauri::command]
fn update_game(id: String, state: tauri::State<LanSpreadState>) -> bool {
let already_in_download = tauri::async_runtime::block_on(async {
if state.inner().games_in_download.read().await.contains(&id) {
log::warn!("Game is already downloading/updating: {id}");
return true;
}
false
});
if already_in_download {
return false;
}
// Get the games folder
let games_folder =
tauri::async_runtime::block_on(async { state.inner().games_folder.read().await.clone() });
let games_folder = PathBuf::from(games_folder);
let game_path = games_folder.join(&id);
// Backup current game folder
let backup_path = match backup_game_folder(&game_path) {
Ok(path) => path,
Err(e) => {
log::error!("Failed to backup game folder for {id}: {e}");
return false;
}
};
// Store backup path for later cleanup/restore
tauri::async_runtime::block_on(async {
state
.inner()
.game_backups
.write()
.await
.insert(id.clone(), backup_path.clone());
});
log::info!("Starting update for game: {id}");
// Start the download process
if let Err(e) = state
.inner()
.client_ctrl
.send(ClientCommand::GetGame(id.clone()))
{
log::error!("Failed to send message to client: {e:?}");
// Try to restore backup if download fails to start
if let Err(restore_err) = restore_game_folder(&game_path, &backup_path) {
log::error!("Failed to restore backup after download failure: {restore_err}");
}
return false;
}
true
}
#[cfg(target_os = "windows")]
fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
@@ -373,6 +506,7 @@ pub fn run() {
games: Arc::new(RwLock::new(GameDB::empty())),
games_in_download: Arc::new(RwLock::new(HashSet::new())),
games_folder: Arc::new(RwLock::new(String::new())),
game_backups: Arc::new(RwLock::new(std::collections::HashMap::new())),
};
tauri::Builder::default()
@@ -384,7 +518,8 @@ pub fn run() {
request_games,
install_game,
run_game,
update_game_directory
update_game_directory,
update_game
])
.manage(lanspread_state)
.setup(|app| {
@@ -453,7 +588,6 @@ pub fn run() {
.await
.remove(&id.clone());
let games_folder = app_handle
.state::<LanSpreadState>()
.inner()
@@ -463,12 +597,33 @@ pub fn run() {
.clone();
if let Ok(sidecar) = app_handle.shell().sidecar("unrar") {
let _games_folder_clone = games_folder.clone();
let id_clone = id.clone();
let app_handle_clone = app_handle.clone();
// Spawn a separate task to handle unpacking and backup cleanup
tauri::async_runtime::spawn(async move {
unpack_game(&id, sidecar, games_folder).await;
// Check if this was an update and clean up backup if successful
let backup_path = app_handle_clone
.state::<LanSpreadState>()
.inner()
.game_backups
.write()
.await
.remove(&id_clone);
if let Some(backup_path) = backup_path
&& let Err(e) = cleanup_backup_folder(&backup_path) {
log::error!("Failed to cleanup backup folder after successful update: {e}");
}
log::info!("ClientEvent::UnpackGameFinished received");
if let Err(e) = app_handle.emit("game-unpack-finished", Some(id.clone())) {
if let Err(e) = app_handle_clone.emit("game-unpack-finished", Some(id_clone.clone())) {
log::error!("ClientEvent::UnpackGameFinished: Failed to emit game-unpack-finished event: {e}");
}
});
}
}
ClientEvent::DownloadGameFilesFailed { id } => {
@@ -485,6 +640,30 @@ pub fn run() {
.write()
.await
.remove(&id.clone());
// Check if this was an update and restore backup if failed
let backup_path = app_handle
.state::<LanSpreadState>()
.inner()
.game_backups
.write()
.await
.remove(&id);
if let Some(backup_path) = backup_path {
let games_folder = app_handle
.state::<LanSpreadState>()
.inner()
.games_folder
.read()
.await
.clone();
let game_path = PathBuf::from(games_folder).join(&id);
if let Err(e) = restore_game_folder(&game_path, &backup_path) {
log::error!("Failed to restore backup after download failure: {e}");
}
}
},
}
}
+2 -2
View File
@@ -209,7 +209,7 @@ const App = () => {
const updateGame = async (id: string) => {
console.log(`🎯 Updating game with id=${id}`);
try {
const success = await invoke('install_game', { id });
const success = await invoke('update_game', { id });
if (success) {
console.log(`✅ Game update for id=${id} started...`);
// update install status in gameItems for this game
@@ -217,7 +217,7 @@ const App = () => {
? { ...item, install_status: InstallStatus.CheckingServer }
: item));
} else {
// game is already being installed
// game is already being installed/updated
console.warn(`🚧 Game with id=${id} is already being updated`);
}
} catch (error) {