Files
creamlinux-installer/src-tauri/src/installer.rs
2025-05-17 22:49:09 +02:00

1075 lines
35 KiB
Rust

use crate::AppState;
use log::{error, info, warn};
use reqwest;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::fs;
use std::io;
use std::path::Path;
use std::sync::atomic::Ordering;
use std::time::Duration;
use tauri::Manager;
use tauri::{AppHandle, Emitter};
use tempfile::tempdir;
use zip::ZipArchive;
// Constants for API endpoints and downloads
const CREAMLINUX_RELEASE_URL: &str =
"https://github.com/anticitizn/creamlinux/releases/latest/download/creamlinux.zip";
const SMOKEAPI_REPO: &str = "acidicoala/SmokeAPI";
// Type of installer
#[derive(Debug, Clone, Copy)]
pub enum InstallerType {
Cream,
Smoke,
}
// Action to perform
#[derive(Debug, Clone, Copy)]
pub enum InstallerAction {
Install,
Uninstall,
}
// Error type combining all possible errors
#[derive(Debug)]
pub enum InstallerError {
IoError(io::Error),
ReqwestError(reqwest::Error),
ZipError(zip::result::ZipError),
InstallationError(String),
}
impl From<io::Error> for InstallerError {
fn from(err: io::Error) -> Self {
InstallerError::IoError(err)
}
}
impl From<reqwest::Error> for InstallerError {
fn from(err: reqwest::Error) -> Self {
InstallerError::ReqwestError(err)
}
}
impl From<zip::result::ZipError> for InstallerError {
fn from(err: zip::result::ZipError) -> Self {
InstallerError::ZipError(err)
}
}
impl std::fmt::Display for InstallerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InstallerError::IoError(e) => write!(f, "IO error: {}", e),
InstallerError::ReqwestError(e) => write!(f, "Network error: {}", e),
InstallerError::ZipError(e) => write!(f, "Zip extraction error: {}", e),
InstallerError::InstallationError(e) => write!(f, "Installation error: {}", e),
}
}
}
impl std::error::Error for InstallerError {}
// DLC Information structure
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DlcInfo {
pub appid: String,
pub name: String,
}
// Struct to hold installation instructions for the frontend
#[derive(Serialize, Debug, Clone)]
pub struct InstallationInstructions {
#[serde(rename = "type")]
pub type_: String,
pub command: String,
pub game_title: String,
pub dlc_count: Option<usize>,
}
// Game information structure from searcher module
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Game {
pub id: String,
pub title: String,
pub path: String,
pub native: bool,
pub api_files: Vec<String>,
pub cream_installed: bool,
pub smoke_installed: bool,
pub installing: bool,
}
// Emit a progress update to the frontend
pub fn emit_progress(
app_handle: &AppHandle,
title: &str,
message: &str,
progress: f32,
complete: bool,
show_instructions: bool,
instructions: Option<InstallationInstructions>,
) {
let mut payload = json!({
"title": title,
"message": message,
"progress": progress,
"complete": complete,
"show_instructions": show_instructions
});
if let Some(inst) = instructions {
payload["instructions"] = serde_json::to_value(inst).unwrap_or_default();
}
if let Err(e) = app_handle.emit("installation-progress", payload) {
warn!("Failed to emit progress event: {}", e);
}
}
// Process a single game action (install/uninstall Cream/Smoke)
pub async fn process_action(
_game_id: String,
installer_type: InstallerType,
action: InstallerAction,
game: Game,
app_handle: AppHandle,
) -> Result<(), String> {
match (installer_type, action) {
(InstallerType::Cream, InstallerAction::Install) => {
// We only allow CreamLinux for native games
if !game.native {
return Err("CreamLinux can only be installed on native Linux games".to_string());
}
info!("Installing CreamLinux for game: {}", game.title);
let game_title = game.title.clone();
emit_progress(
&app_handle,
&format!("Installing CreamLinux for {}", game_title),
"Fetching DLC list...",
10.0,
false,
false,
None,
);
// Fetch DLC list
let dlcs = match fetch_dlc_details(&game.id).await {
Ok(dlcs) => dlcs,
Err(e) => {
error!("Failed to fetch DLC details: {}", e);
return Err(format!("Failed to fetch DLC details: {}", e));
}
};
let dlc_count = dlcs.len();
info!("Found {} DLCs for {}", dlc_count, game_title);
emit_progress(
&app_handle,
&format!("Installing CreamLinux for {}", game_title),
"Downloading CreamLinux...",
30.0,
false,
false,
None,
);
// Install CreamLinux
let app_handle_clone = app_handle.clone();
let game_title_clone = game_title.clone();
match install_creamlinux(&game.path, &game.id, dlcs, move |progress, message| {
// Emit progress updates during installation
emit_progress(
&app_handle_clone,
&format!("Installing CreamLinux for {}", game_title_clone),
message,
30.0 + (progress * 60.0), // Scale progress from 30% to 90%
false,
false,
None,
);
})
.await
{
Ok(_) => {
// Emit completion with instructions
let instructions = InstallationInstructions {
type_: "cream_install".to_string(),
command: "sh ./cream.sh %command%".to_string(),
game_title: game_title.clone(),
dlc_count: Some(dlc_count),
};
emit_progress(
&app_handle,
&format!("Installation Completed: {}", game_title),
"CreamLinux has been installed successfully!",
100.0,
true,
true,
Some(instructions),
);
info!("CreamLinux installation completed for: {}", game_title);
Ok(())
}
Err(e) => {
error!("Failed to install CreamLinux: {}", e);
Err(format!("Failed to install CreamLinux: {}", e))
}
}
}
(InstallerType::Cream, InstallerAction::Uninstall) => {
// Ensure this is a native game
if !game.native {
return Err(
"CreamLinux can only be uninstalled from native Linux games".to_string()
);
}
let game_title = game.title.clone();
info!("Uninstalling CreamLinux from game: {}", game_title);
emit_progress(
&app_handle,
&format!("Uninstalling CreamLinux from {}", game_title),
"Removing CreamLinux files...",
30.0,
false,
false,
None,
);
// Uninstall CreamLinux
match uninstall_creamlinux(&game.path) {
Ok(_) => {
// Emit completion with instructions
let instructions = InstallationInstructions {
type_: "cream_uninstall".to_string(),
command: "sh ./cream.sh %command%".to_string(),
game_title: game_title.clone(),
dlc_count: None,
};
emit_progress(
&app_handle,
&format!("Uninstallation Completed: {}", game_title),
"CreamLinux has been uninstalled successfully!",
100.0,
true,
true,
Some(instructions),
);
info!("CreamLinux uninstallation completed for: {}", game_title);
Ok(())
}
Err(e) => {
error!("Failed to uninstall CreamLinux: {}", e);
Err(format!("Failed to uninstall CreamLinux: {}", e))
}
}
}
(InstallerType::Smoke, InstallerAction::Install) => {
// We only allow SmokeAPI for Proton/Windows games
if game.native {
return Err("SmokeAPI can only be installed on Proton/Windows games".to_string());
}
// Check if we have any Steam API DLLs to patch
if game.api_files.is_empty() {
return Err(
"No Steam API DLLs found to patch. SmokeAPI cannot be installed.".to_string(),
);
}
let game_title = game.title.clone();
info!("Installing SmokeAPI for game: {}", game_title);
emit_progress(
&app_handle,
&format!("Installing SmokeAPI for {}", game_title),
"Fetching SmokeAPI release information...",
10.0,
false,
false,
None,
);
// Create clones for the closure
let app_handle_clone = app_handle.clone();
let game_title_clone = game_title.clone();
let api_files = game.api_files.clone();
// Call the SmokeAPI installation with progress updates
match install_smokeapi(&game.path, &api_files, move |progress, message| {
// Emit progress updates during installation
emit_progress(
&app_handle_clone,
&format!("Installing SmokeAPI for {}", game_title_clone),
message,
10.0 + (progress * 90.0), // Scale progress from 10% to 100%
false,
false,
None,
);
})
.await
{
Ok(_) => {
// Emit completion with instructions
let instructions = InstallationInstructions {
type_: "smoke_install".to_string(),
command: "No additional steps needed. SmokeAPI will work automatically."
.to_string(),
game_title: game_title.clone(),
dlc_count: Some(game.api_files.len()),
};
emit_progress(
&app_handle,
&format!("Installation Completed: {}", game_title),
"SmokeAPI has been installed successfully!",
100.0,
true,
true,
Some(instructions),
);
info!("SmokeAPI installation completed for: {}", game_title);
Ok(())
}
Err(e) => {
error!("Failed to install SmokeAPI: {}", e);
Err(format!("Failed to install SmokeAPI: {}", e))
}
}
}
(InstallerType::Smoke, InstallerAction::Uninstall) => {
// Ensure this is a non-native game
if game.native {
return Err(
"SmokeAPI can only be uninstalled from Proton/Windows games".to_string()
);
}
let game_title = game.title.clone();
info!("Uninstalling SmokeAPI from game: {}", game_title);
emit_progress(
&app_handle,
&format!("Uninstalling SmokeAPI from {}", game_title),
"Restoring original files...",
30.0,
false,
false,
None,
);
// Uninstall SmokeAPI
match uninstall_smokeapi(&game.path, &game.api_files) {
Ok(_) => {
// Emit completion with instructions
let instructions = InstallationInstructions {
type_: "smoke_uninstall".to_string(),
command: "Original Steam API files have been restored.".to_string(),
game_title: game_title.clone(),
dlc_count: None,
};
emit_progress(
&app_handle,
&format!("Uninstallation Completed: {}", game_title),
"SmokeAPI has been uninstalled successfully!",
100.0,
true,
true,
Some(instructions),
);
info!("SmokeAPI uninstallation completed for: {}", game_title);
Ok(())
}
Err(e) => {
error!("Failed to uninstall SmokeAPI: {}", e);
Err(format!("Failed to uninstall SmokeAPI: {}", e))
}
}
}
}
}
// Install CreamLinux for a game
async fn install_creamlinux<F>(
game_path: &str,
app_id: &str,
dlcs: Vec<DlcInfo>,
progress_callback: F,
) -> Result<(), InstallerError>
where
F: Fn(f32, &str) + Send + 'static,
{
// Progress update
progress_callback(0.1, "Preparing to download CreamLinux...");
// Download CreamLinux zip
let client = reqwest::Client::new();
progress_callback(0.2, "Downloading CreamLinux...");
let response = client
.get(CREAMLINUX_RELEASE_URL)
.timeout(Duration::from_secs(30))
.send()
.await?;
if !response.status().is_success() {
return Err(InstallerError::InstallationError(format!(
"Failed to download CreamLinux: HTTP {}",
response.status()
)));
}
// Save to temporary file
progress_callback(0.4, "Saving downloaded files...");
let temp_dir = tempdir()?;
let zip_path = temp_dir.path().join("creamlinux.zip");
let content = response.bytes().await?;
fs::write(&zip_path, &content)?;
// Extract the zip
progress_callback(0.5, "Extracting CreamLinux files...");
let file = fs::File::open(&zip_path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = Path::new(game_path).join(file.name());
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = fs::File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
}
// Set executable permissions for cream.sh
if file.name() == "cream.sh" {
progress_callback(0.6, "Setting executable permissions...");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&outpath)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&outpath, perms)?;
}
}
}
// Create cream_api.ini with DLC info
progress_callback(0.8, "Creating configuration file...");
let cream_api_path = Path::new(game_path).join("cream_api.ini");
let mut config = String::new();
config.push_str(&format!("APPID = {}\n[config]\n", app_id));
config.push_str("issubscribedapp_on_false_use_real = true\n");
config.push_str("[methods]\n");
config.push_str("disable_steamapps_issubscribedapp = false\n");
config.push_str("[dlc]\n");
for dlc in dlcs {
config.push_str(&format!("{} = {}\n", dlc.appid, dlc.name));
}
fs::write(cream_api_path, config)?;
progress_callback(1.0, "Installation completed successfully!");
Ok(())
}
// Install CreamLinux for a game with pre-fetched DLC list
pub async fn install_creamlinux_with_dlcs<F>(
game_path: &str,
app_id: &str,
dlcs: Vec<DlcInfo>,
progress_callback: F,
) -> Result<(), InstallerError>
where
F: Fn(f32, &str) + Send + 'static,
{
// Progress update
progress_callback(0.1, "Preparing to download CreamLinux...");
// Download CreamLinux zip
let client = reqwest::Client::new();
progress_callback(0.2, "Downloading CreamLinux...");
let response = client
.get(CREAMLINUX_RELEASE_URL)
.timeout(Duration::from_secs(30))
.send()
.await?;
if !response.status().is_success() {
return Err(InstallerError::InstallationError(format!(
"Failed to download CreamLinux: HTTP {}",
response.status()
)));
}
// Save to temporary file
progress_callback(0.4, "Saving downloaded files...");
let temp_dir = tempdir()?;
let zip_path = temp_dir.path().join("creamlinux.zip");
let content = response.bytes().await?;
fs::write(&zip_path, &content)?;
// Extract the zip
progress_callback(0.5, "Extracting CreamLinux files...");
let file = fs::File::open(&zip_path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = Path::new(game_path).join(file.name());
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = fs::File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
}
// Set executable permissions for cream.sh
if file.name() == "cream.sh" {
progress_callback(0.6, "Setting executable permissions...");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&outpath)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&outpath, perms)?;
}
}
}
// Create cream_api.ini with DLC info - using the provided DLCs directly
progress_callback(0.8, "Creating configuration file...");
let cream_api_path = Path::new(game_path).join("cream_api.ini");
let mut config = String::new();
config.push_str(&format!("APPID = {}\n[config]\n", app_id));
config.push_str("issubscribedapp_on_false_use_real = true\n");
config.push_str("[methods]\n");
config.push_str("disable_steamapps_issubscribedapp = false\n");
config.push_str("[dlc]\n");
for dlc in dlcs {
config.push_str(&format!("{} = {}\n", dlc.appid, dlc.name));
}
fs::write(cream_api_path, config)?;
progress_callback(1.0, "Installation completed successfully!");
Ok(())
}
// Uninstall CreamLinux from a game
fn uninstall_creamlinux(game_path: &str) -> Result<(), InstallerError> {
info!("Uninstalling CreamLinux from: {}", game_path);
// Files to remove during uninstallation
let files_to_remove = [
"cream.sh",
"cream_api.ini",
"cream_api.so",
"lib32Creamlinux.so",
"lib64Creamlinux.so",
];
for file in &files_to_remove {
let file_path = Path::new(game_path).join(file);
if file_path.exists() {
match fs::remove_file(&file_path) {
Ok(_) => info!("Removed file: {}", file_path.display()),
Err(e) => {
error!("Failed to remove {}: {}", file_path.display(), e);
// Continue with other files even if one fails
}
}
}
}
info!("CreamLinux uninstallation completed for: {}", game_path);
Ok(())
}
// Fetch DLC details from Steam API
pub async fn fetch_dlc_details(app_id: &str) -> Result<Vec<DlcInfo>, InstallerError> {
let client = reqwest::Client::new();
let base_url = format!(
"https://store.steampowered.com/api/appdetails?appids={}",
app_id
);
let response = client
.get(&base_url)
.timeout(Duration::from_secs(10))
.send()
.await?;
if !response.status().is_success() {
return Err(InstallerError::InstallationError(format!(
"Failed to fetch game details: HTTP {}",
response.status()
)));
}
let data: serde_json::Value = response.json().await?;
let dlc_ids = match data
.get(app_id)
.and_then(|app| app.get("data"))
.and_then(|data| data.get("dlc"))
{
Some(dlc_array) => match dlc_array.as_array() {
Some(array) => array
.iter()
.filter_map(|id| id.as_u64().map(|n| n.to_string()))
.collect::<Vec<String>>(),
_ => Vec::new(),
},
_ => Vec::new(),
};
info!("Found {} DLCs for game ID {}", dlc_ids.len(), app_id);
let mut dlc_details = Vec::new();
for dlc_id in dlc_ids {
let dlc_url = format!(
"https://store.steampowered.com/api/appdetails?appids={}",
dlc_id
);
// Add a small delay to avoid rate limiting
tokio::time::sleep(Duration::from_millis(300)).await;
let dlc_response = client
.get(&dlc_url)
.timeout(Duration::from_secs(10))
.send()
.await?;
if dlc_response.status().is_success() {
let dlc_data: serde_json::Value = dlc_response.json().await?;
let dlc_name = match dlc_data
.get(&dlc_id)
.and_then(|app| app.get("data"))
.and_then(|data| data.get("name"))
{
Some(name) => match name.as_str() {
Some(s) => s.to_string(),
_ => "Unknown DLC".to_string(),
},
_ => "Unknown DLC".to_string(),
};
info!("Found DLC: {} ({})", dlc_name, dlc_id);
dlc_details.push(DlcInfo {
appid: dlc_id,
name: dlc_name,
});
} else if dlc_response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
// If rate limited, wait longer
error!("Rate limited by Steam API, waiting 10 seconds");
tokio::time::sleep(Duration::from_secs(10)).await;
}
}
info!(
"Successfully retrieved details for {} DLCs",
dlc_details.len()
);
Ok(dlc_details)
}
// Fetch DLC details from Steam API with progress updates
pub async fn fetch_dlc_details_with_progress(
app_id: &str,
app_handle: &tauri::AppHandle,
) -> Result<Vec<DlcInfo>, InstallerError> {
info!(
"Starting DLC details fetch with progress for game ID: {}",
app_id
);
// Get a reference to a cancellation flag from app state
let state = app_handle.state::<AppState>();
let should_cancel = state.fetch_cancellation.clone();
let client = reqwest::Client::new();
let base_url = format!(
"https://store.steampowered.com/api/appdetails?appids={}",
app_id
);
// Emit initial progress
emit_dlc_progress(app_handle, "Looking up game details...", 5, None);
info!("Emitted initial DLC progress: 5%");
let response = client
.get(&base_url)
.timeout(Duration::from_secs(10))
.send()
.await?;
if !response.status().is_success() {
let error_msg = format!("Failed to fetch game details: HTTP {}", response.status());
error!("{}", error_msg);
return Err(InstallerError::InstallationError(error_msg));
}
let data: serde_json::Value = response.json().await?;
let dlc_ids = match data
.get(app_id)
.and_then(|app| app.get("data"))
.and_then(|data| data.get("dlc"))
{
Some(dlc_array) => match dlc_array.as_array() {
Some(array) => array
.iter()
.filter_map(|id| id.as_u64().map(|n| n.to_string()))
.collect::<Vec<String>>(),
_ => Vec::new(),
},
_ => Vec::new(),
};
info!("Found {} DLCs for game ID {}", dlc_ids.len(), app_id);
emit_dlc_progress(
app_handle,
&format!("Found {} DLCs. Fetching details...", dlc_ids.len()),
10,
None,
);
info!("Emitted DLC progress: 10%, found {} DLCs", dlc_ids.len());
let mut dlc_details = Vec::new();
let total_dlcs = dlc_ids.len();
for (index, dlc_id) in dlc_ids.iter().enumerate() {
// Check if cancellation was requested
if should_cancel.load(Ordering::SeqCst) {
info!("DLC fetch cancelled for game {}", app_id);
return Err(InstallerError::InstallationError(
"Operation cancelled by user".to_string(),
));
}
let progress_percent = 10.0 + (index as f32 / total_dlcs as f32) * 90.0;
let progress_rounded = progress_percent as u32;
let remaining_dlcs = total_dlcs - index;
// Estimate time remaining (rough calculation - 300ms per DLC)
let est_time_left = if remaining_dlcs > 0 {
let seconds = (remaining_dlcs as f32 * 0.3).ceil() as u32;
if seconds < 60 {
format!("~{} seconds", seconds)
} else {
format!("~{} minute(s)", (seconds as f32 / 60.0).ceil() as u32)
}
} else {
"almost done".to_string()
};
info!(
"Processing DLC {}/{} - Progress: {}%",
index + 1,
total_dlcs,
progress_rounded
);
emit_dlc_progress(
app_handle,
&format!("Processing DLC {}/{}", index + 1, total_dlcs),
progress_rounded,
Some(&est_time_left),
);
let dlc_url = format!(
"https://store.steampowered.com/api/appdetails?appids={}",
dlc_id
);
// Add a small delay to avoid rate limiting
tokio::time::sleep(Duration::from_millis(300)).await;
let dlc_response = client
.get(&dlc_url)
.timeout(Duration::from_secs(10))
.send()
.await?;
if dlc_response.status().is_success() {
let dlc_data: serde_json::Value = dlc_response.json().await?;
let dlc_name = match dlc_data
.get(&dlc_id)
.and_then(|app| app.get("data"))
.and_then(|data| data.get("name"))
{
Some(name) => match name.as_str() {
Some(s) => s.to_string(),
_ => "Unknown DLC".to_string(),
},
_ => "Unknown DLC".to_string(),
};
info!("Found DLC: {} ({})", dlc_name, dlc_id);
let dlc_info = DlcInfo {
appid: dlc_id.clone(),
name: dlc_name,
};
// Emit each DLC as we find it
if let Ok(json) = serde_json::to_string(&dlc_info) {
if let Err(e) = app_handle.emit("dlc-found", json) {
warn!("Failed to emit dlc-found event: {}", e);
} else {
info!("Emitted dlc-found event for DLC: {}", dlc_id);
}
}
dlc_details.push(dlc_info);
} else if dlc_response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
// If rate limited, wait longer
error!("Rate limited by Steam API, waiting 10 seconds");
emit_dlc_progress(
app_handle,
"Rate limited by Steam. Waiting...",
progress_rounded,
None,
);
tokio::time::sleep(Duration::from_secs(10)).await;
}
}
// Final progress update
info!(
"Completed DLC fetch. Found {} DLCs in total",
dlc_details.len()
);
emit_dlc_progress(
app_handle,
&format!("Completed! Found {} DLCs", dlc_details.len()),
100,
None,
);
info!("Emitted final DLC progress: 100%");
Ok(dlc_details)
}
// Emit DLC progress updates to the frontend
fn emit_dlc_progress(
app_handle: &tauri::AppHandle,
message: &str,
progress: u32,
time_left: Option<&str>,
) {
let mut payload = json!({
"message": message,
"progress": progress
});
if let Some(time) = time_left {
payload["timeLeft"] = json!(time);
}
if let Err(e) = app_handle.emit("dlc-progress", payload) {
warn!("Failed to emit dlc-progress event: {}", e);
}
}
// Install SmokeAPI for a game
async fn install_smokeapi<F>(
game_path: &str,
api_files: &[String],
progress_callback: F,
) -> Result<(), InstallerError>
where
F: Fn(f32, &str) + Send + 'static,
{
// Get the latest SmokeAPI release
progress_callback(0.1, "Fetching latest SmokeAPI release...");
let client = reqwest::Client::new();
let releases_url = format!(
"https://api.github.com/repos/{}/releases/latest",
SMOKEAPI_REPO
);
let response = client
.get(&releases_url)
.header("User-Agent", "CreamLinux")
.timeout(Duration::from_secs(10))
.send()
.await?;
if !response.status().is_success() {
return Err(InstallerError::InstallationError(format!(
"Failed to fetch SmokeAPI releases: HTTP {}",
response.status()
)));
}
let release_info: serde_json::Value = response.json().await?;
let latest_version = match release_info.get("tag_name") {
Some(tag) => tag.as_str().unwrap_or("latest"),
_ => "latest",
};
info!("Latest SmokeAPI version: {}", latest_version);
// Construct download URL
let zip_url = format!(
"https://github.com/{}/releases/download/{}/SmokeAPI-{}.zip",
SMOKEAPI_REPO, latest_version, latest_version
);
// Download the zip
progress_callback(0.3, "Downloading SmokeAPI...");
let response = client
.get(&zip_url)
.timeout(Duration::from_secs(30))
.send()
.await?;
if !response.status().is_success() {
return Err(InstallerError::InstallationError(format!(
"Failed to download SmokeAPI: HTTP {}",
response.status()
)));
}
// Save to temporary file
progress_callback(0.5, "Saving downloaded files...");
let temp_dir = tempdir()?;
let zip_path = temp_dir.path().join("smokeapi.zip");
let content = response.bytes().await?;
fs::write(&zip_path, &content)?;
// Extract and install for each API file
progress_callback(0.6, "Extracting SmokeAPI files...");
let file = fs::File::open(&zip_path)?;
let mut archive = ZipArchive::new(file)?;
for (i, api_file) in api_files.iter().enumerate() {
let progress = 0.6 + (i as f32 / api_files.len() as f32) * 0.3;
progress_callback(progress, &format!("Installing SmokeAPI for {}", api_file));
let api_dir = Path::new(game_path).join(
Path::new(api_file)
.parent()
.unwrap_or_else(|| Path::new("")),
);
let api_name = Path::new(api_file).file_name().unwrap_or_default();
// Backup original file
let original_path = api_dir.join(api_name);
let backup_path = api_dir.join(api_name.to_string_lossy().replace(".dll", "_o.dll"));
info!("Processing: {}", original_path.display());
info!("Backup path: {}", backup_path.display());
// Only backup if not already backed up
if !backup_path.exists() && original_path.exists() {
fs::copy(&original_path, &backup_path)?;
info!("Created backup: {}", backup_path.display());
}
// Extract the appropriate DLL directly to the game directory
if let Ok(mut file) = archive.by_name(&api_name.to_string_lossy()) {
let mut outfile = fs::File::create(&original_path)?;
io::copy(&mut file, &mut outfile)?;
info!("Installed SmokeAPI as: {}", original_path.display());
} else {
return Err(InstallerError::InstallationError(format!(
"Could not find {} in the SmokeAPI zip file",
api_name.to_string_lossy()
)));
}
}
progress_callback(1.0, "SmokeAPI installation completed!");
info!("SmokeAPI installation completed for: {}", game_path);
Ok(())
}
// Uninstall SmokeAPI from a game
fn uninstall_smokeapi(game_path: &str, api_files: &[String]) -> Result<(), InstallerError> {
info!("Uninstalling SmokeAPI from: {}", game_path);
for api_file in api_files {
let api_path = Path::new(game_path).join(api_file);
let api_dir = api_path.parent().unwrap_or_else(|| Path::new(game_path));
let api_name = api_path.file_name().unwrap_or_default();
let original_path = api_dir.join(api_name);
let backup_path = api_dir.join(api_name.to_string_lossy().replace(".dll", "_o.dll"));
info!("Processing: {}", original_path.display());
info!("Backup path: {}", backup_path.display());
if backup_path.exists() {
// Remove the SmokeAPI version
if original_path.exists() {
match fs::remove_file(&original_path) {
Ok(_) => info!("Removed SmokeAPI file: {}", original_path.display()),
Err(e) => error!(
"Failed to remove SmokeAPI file: {}, error: {}",
original_path.display(),
e
),
}
}
// Restore the original file
match fs::rename(&backup_path, &original_path) {
Ok(_) => info!("Restored original file: {}", original_path.display()),
Err(e) => {
error!(
"Failed to restore original file: {}, error: {}",
original_path.display(),
e
);
// Try to copy instead if rename fails
if let Err(copy_err) = fs::copy(&backup_path, &original_path)
.and_then(|_| fs::remove_file(&backup_path))
{
error!("Failed to copy backup file: {}", copy_err);
}
}
}
} else {
info!("No backup found for: {}", api_file);
}
}
info!("SmokeAPI uninstallation completed for: {}", game_path);
Ok(())
}