backend: cleanup error handling hacks on the rust layer

This commit is contained in:
Tyler Wilding 2023-02-25 16:06:20 -05:00
parent 920b45bab0
commit e13c12b895
No known key found for this signature in database
GPG key ID: 77CB07796494137E
20 changed files with 861 additions and 608 deletions

1
src-tauri/Cargo.lock generated
View file

@ -2156,6 +2156,7 @@ dependencies = [
"sysinfo",
"tauri",
"tauri-build",
"thiserror",
"tokio",
"walkdir",
"wgpu",

View file

@ -30,6 +30,7 @@ sysinfo = "0.28.0"
wgpu = "0.15.1"
walkdir = "2.3.2"
dir-diff = "0.3.2"
thiserror = "1.0.38"
[features]
# by default Tauri runs in production mode

View file

@ -1,67 +1,41 @@
use fs_extra::dir::copy;
use serde::{Deserialize, Serialize};
use tauri::command;
use tauri::Manager;
use crate::util::open_dir_in_os;
use serde::{Serialize, Serializer};
pub mod binaries;
pub mod config;
pub mod extractor;
pub mod game;
pub mod support;
pub mod versions;
pub mod window;
#[derive(Serialize, Deserialize, Debug)]
#[derive(Debug, thiserror::Error)]
pub enum CommandError {
ArchitectureNotx86,
AVXNotSupported,
Unknown,
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
NetworkRequest(#[from] reqwest::Error),
#[error("{0}")]
Configuration(String),
#[error(transparent)]
TauriEvent(#[from] tauri::Error),
#[error("{0}")]
Installation(String),
#[error("{0}")]
VersionManagement(String),
#[error("{0}")]
InvalidPath(String),
#[error("{0}")]
BinaryExecution(String),
#[error("{0}")]
Support(String),
#[error("{0}")]
Other(String),
}
#[command]
pub async fn get_highest_simd() -> Result<String, CommandError> {
return highest_simd().await;
}
#[cfg(target_arch = "x86_64")]
async fn highest_simd() -> Result<String, CommandError> {
if is_x86_feature_detected!("avx2") {
return Ok("AVX2".to_string());
} else if is_x86_feature_detected!("avx") {
return Ok("AVX".to_string());
} else {
return Err(CommandError::AVXNotSupported);
impl Serialize for CommandError {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[cfg(not(target_arch = "x86_64"))]
fn highest_simd() -> Result<String, CommandError> {
return Err(CommandError::ArchitectureNotx86);
}
#[command]
pub fn open_dir(dir: String) {
return open_dir_in_os(dir);
}
#[command]
pub async fn copy_dir(dir_src: String, dir_dest: String) -> bool {
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true;
options.overwrite = true;
options.content_only = true;
if let Err(_e) = copy(dir_src, dir_dest, &options) {
return false;
}
return true;
}
#[tauri::command]
pub async fn close_splashscreen(window: tauri::Window) {
// Close splashscreen
if let Some(splashscreen) = window.get_window("splashscreen") {
splashscreen.close().unwrap();
}
// Show main window
window.get_window("main").unwrap().show().unwrap();
}

View file

@ -0,0 +1,260 @@
use std::{
path::{Path, PathBuf},
process::Command,
};
use crate::config::LauncherConfig;
use super::CommandError;
// TODO - update data dir command
fn bin_ext(filename: &str) -> String {
if cfg!(windows) {
return format!("{}.exe", filename);
}
return filename.to_string();
}
struct CommonConfigData {
install_path: std::path::PathBuf,
active_version: String,
active_version_folder: String,
}
fn common_prelude(
config: &tokio::sync::MutexGuard<LauncherConfig>,
) -> Result<CommonConfigData, CommandError> {
let install_path = match &config.installation_dir {
None => {
return Err(CommandError::BinaryExecution(format!(
"No installation directory set, can't perform operation"
)))
}
Some(path) => Path::new(path),
};
let active_version = config
.active_version
.as_ref()
.ok_or(CommandError::BinaryExecution(format!(
"No active version set, can't perform operation"
)))?;
let active_version_folder =
config
.active_version_folder
.as_ref()
.ok_or(CommandError::BinaryExecution(format!(
"No active version folder set, can't perform operation"
)))?;
Ok(CommonConfigData {
install_path: install_path.to_path_buf(),
active_version: active_version.clone(),
active_version_folder: active_version_folder.clone(),
})
}
fn get_data_dir(
config_info: &CommonConfigData,
game_name: &String,
) -> Result<PathBuf, CommandError> {
let data_folder = config_info
.install_path
.join("active")
.join(game_name)
.join("data");
if !data_folder.exists() {
return Err(CommandError::BinaryExecution(format!(
"Could not locate relevant data directory '{}', can't perform operation",
data_folder.to_string_lossy()
)));
}
Ok(data_folder)
}
struct ExecutableLocation {
executable_dir: PathBuf,
executable_path: PathBuf,
}
fn get_exec_location(
config_info: &CommonConfigData,
executable_name: &str,
) -> Result<ExecutableLocation, CommandError> {
let exec_dir = config_info
.install_path
.join("versions")
.join(&config_info.active_version_folder)
.join(&config_info.active_version);
let exec_path = exec_dir.join(bin_ext(executable_name));
if !exec_path.exists() {
return Err(CommandError::BinaryExecution(format!(
"Could not find the required binary '{}', can't perform operation",
exec_path.to_string_lossy()
)));
}
Ok(ExecutableLocation {
executable_dir: exec_dir,
executable_path: exec_path,
})
}
#[tauri::command]
pub async fn extract_and_validate_iso(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
path_to_iso: String,
game_name: String,
) -> Result<(), CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
let data_folder = get_data_dir(&config_info, &game_name)?;
let exec_info = get_exec_location(&config_info, "extractor")?;
let mut args = vec![
path_to_iso.clone(),
"--extract".to_string(),
"--validate".to_string(),
"--proj-path".to_string(),
data_folder.to_string_lossy().into_owned(),
];
if Path::new(&path_to_iso.clone()).is_dir() {
args.push("--folder".to_string());
}
// TODO - tee logs and handle error codes
let output = Command::new(exec_info.executable_path)
.args(args)
.current_dir(exec_info.executable_dir)
.output()
.expect("failed to execute process");
Ok(())
}
#[tauri::command]
pub async fn run_decompiler(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
path_to_iso: String,
game_name: String,
) -> Result<(), CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
let data_folder = get_data_dir(&config_info, &game_name)?;
let exec_info = get_exec_location(&config_info, "extractor")?;
let mut source_path = path_to_iso;
if source_path.is_empty() {
source_path = data_folder
.join("iso_data")
.join(game_name)
.to_string_lossy()
.to_string();
}
// TODO - tee logs and handle error codes
let output = Command::new(&exec_info.executable_path)
.args([
source_path,
"--decompile".to_string(),
"--proj-path".to_string(),
data_folder.to_string_lossy().into_owned(),
])
.current_dir(exec_info.executable_dir)
.output()
.expect("failed to execute process");
Ok(())
}
#[tauri::command]
pub async fn run_compiler(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
path_to_iso: String,
game_name: String,
) -> Result<(), CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
let data_folder = get_data_dir(&config_info, &game_name)?;
let exec_info = get_exec_location(&config_info, "extractor")?;
let mut source_path = path_to_iso;
if source_path.is_empty() {
source_path = data_folder
.join("iso_data")
.join(game_name)
.to_string_lossy()
.to_string();
}
// TODO - tee logs and handle error codes
let output = Command::new(&exec_info.executable_path)
.args([
source_path,
"--compile".to_string(),
"--proj-path".to_string(),
data_folder.to_string_lossy().into_owned(),
])
.current_dir(exec_info.executable_dir)
.output()
.expect("failed to execute process");
Ok(())
}
#[tauri::command]
pub async fn open_repl(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
) -> Result<(), CommandError> {
// TODO - explore a linux option though this is very annoying because without doing a ton of research
// we seem to have to handle various terminals. Which honestly we should probably do on windows too
//
// So maybe we can make a menu where the user will specify what terminal to use / what launch-options to use
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
let data_folder = get_data_dir(&config_info, &game_name)?;
let exec_info = get_exec_location(&config_info, "goalc")?;
// TODO - handle error
let output = Command::new("cmd")
.args([
"/K",
"start",
&bin_ext("goalc"),
"--proj-path",
&data_folder.to_string_lossy().into_owned(),
])
.current_dir(exec_info.executable_dir)
.spawn()
.expect("failed to execute process");
Ok(())
}
#[tauri::command]
pub async fn launch_game(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
in_debug: bool,
) -> Result<(), CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
let data_folder = get_data_dir(&config_info, &game_name)?;
let exec_info = get_exec_location(&config_info, "gk")?;
let mut args = vec!["-boot".to_string(), "-fakeiso".to_string()];
// TODO - order unfortunately matters for gk args, this will be fixed eventually...
if in_debug {
args.push("-debug".to_string());
}
args.push("-proj-path".to_string());
args.push(data_folder.to_string_lossy().into_owned());
// TODO - tee logs for SURE
let output = Command::new(exec_info.executable_path)
.args(args)
.current_dir(exec_info.executable_dir)
.spawn()
.expect("failed to execute process");
Ok(())
}

View file

@ -1,16 +1,16 @@
use crate::config::LauncherConfig;
use tauri::Manager;
use super::CommandError;
#[tauri::command]
pub async fn get_install_directory(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<String>, ()> {
) -> Result<Option<String>, CommandError> {
let config_lock = config.lock().await;
match config_lock.installation_dir {
match &config_lock.installation_dir {
None => Ok(None),
Some(_) => Ok(Some(
config_lock.installation_dir.as_ref().unwrap().to_string(),
)),
Some(dir) => Ok(Some(dir.to_string())),
}
}
@ -18,27 +18,50 @@ pub async fn get_install_directory(
pub async fn set_install_directory(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
new_dir: String,
) -> Result<(), ()> {
) -> Result<(), CommandError> {
let mut config_lock = config.lock().await;
config_lock.set_install_directory(new_dir);
config_lock.set_install_directory(new_dir).map_err(|_| {
CommandError::Configuration(format!("Unable to persist installation directory"))
})?;
Ok(())
}
#[tauri::command]
pub async fn is_avx_supported() -> Result<bool, ()> {
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
return Ok(true);
} else {
return Ok(false);
}
}
#[tauri::command]
pub async fn is_avx_requirement_met(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<bool>, ()> {
let config_lock = config.lock().await;
) -> Result<bool, CommandError> {
let mut config_lock = config.lock().await;
match config_lock.requirements.avx {
None => Ok(None),
Some(_) => Ok(config_lock.requirements.avx),
None => {
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
config_lock.requirements.avx = Some(false);
} else {
config_lock.requirements.avx = Some(false);
}
config_lock.save_config().map_err(|_| {
CommandError::Configuration(format!("Unable to persist avx requirement change"))
})?;
Ok(config_lock.requirements.avx.unwrap_or(false))
}
Some(val) => Ok(val),
}
}
// TODO - investigate moving the OpenGL check into the rust layer via `wgpu`
// for now, we return potentially undefined so the frontend can update the value via sidecar
#[tauri::command]
pub async fn is_opengl_requirement_met(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<bool>, ()> {
) -> Result<Option<bool>, CommandError> {
let config_lock = config.lock().await;
match config_lock.requirements.opengl {
None => Ok(None),
@ -46,15 +69,33 @@ pub async fn is_opengl_requirement_met(
}
}
#[tauri::command]
pub async fn set_opengl_requirement_met(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
requirement_met: bool,
) -> Result<(), CommandError> {
let mut config_lock = config.lock().await;
config_lock
.set_opengl_requirement_met(requirement_met)
.map_err(|_| {
CommandError::Configuration(format!("Unable to persist opengl requirement change"))
})?;
Ok(())
}
#[tauri::command]
pub async fn finalize_installation(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
app_handle: tauri::AppHandle,
game_name: String,
) -> Result<(), ()> {
) -> Result<(), CommandError> {
let mut config_lock = config.lock().await;
config_lock.update_installed_game_version(game_name, true);
app_handle.emit_all("gameInstalled", {}).unwrap();
config_lock
.update_installed_game_version(game_name, true)
.map_err(|_| {
CommandError::Configuration(format!("Unable to persist game installation status"))
})?;
app_handle.emit_all("gameInstalled", {})?;
Ok(())
}
@ -62,7 +103,7 @@ pub async fn finalize_installation(
pub async fn is_game_installed(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
) -> Result<bool, ()> {
) -> Result<bool, CommandError> {
let config_lock = config.lock().await;
Ok(config_lock.is_game_installed(game_name))
}
@ -71,7 +112,7 @@ pub async fn is_game_installed(
pub async fn get_installed_version(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
) -> Result<String, ()> {
) -> Result<String, CommandError> {
let config_lock = config.lock().await;
// TODO - seriously, convert the config into a damn map
match game_name.as_str() {
@ -87,8 +128,9 @@ pub async fn get_installed_version(
pub async fn get_installed_version_folder(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
) -> Result<String, ()> {
) -> Result<String, CommandError> {
let config_lock = config.lock().await;
// TODO - seriously, convert the config into a damn map
match game_name.as_str() {
"jak1" => Ok(config_lock.games.jak1.version_folder.clone().unwrap()),
"jak2" => Ok(config_lock.games.jak2.version_folder.clone().unwrap()),
@ -97,3 +139,39 @@ pub async fn get_installed_version_folder(
_ => Ok("".to_string()),
}
}
#[tauri::command]
pub async fn save_active_version_change(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
app_handle: tauri::AppHandle,
version_folder: String,
new_active_version: String,
) -> Result<(), CommandError> {
let mut config_lock = config.lock().await;
config_lock
.set_active_version_folder(version_folder)
.map_err(|_| {
CommandError::Configuration(format!("Unable to persist active version folder change"))
})?;
config_lock
.set_active_version(new_active_version)
.map_err(|_| CommandError::Configuration(format!("Unable to persist active version change")))?;
app_handle.emit_all("toolingVersionChanged", {})?;
Ok(())
}
#[tauri::command]
pub async fn get_active_version(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<String>, CommandError> {
let config_lock = config.lock().await;
Ok(config_lock.active_version.clone())
}
#[tauri::command]
pub async fn get_active_version_folder(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<String>, CommandError> {
let config_lock = config.lock().await;
Ok(config_lock.active_version_folder.clone())
}

View file

@ -1,127 +0,0 @@
use std::{path::Path, process::Command};
use crate::config::LauncherConfig;
#[tauri::command]
pub async fn extract_and_validate_iso(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
path_to_iso: String,
game_name: String,
) -> Result<(), ()> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
// TODO - be smarter
// TODO - make folder if it doesnt exist
// TODO - copy over the data folder
// TODO - log it to a file
// TODO - check error code
let install_path = Path::new(path);
let binary_dir = install_path.join("versions/official/v0.1.32/");
let data_folder = install_path.join("active/jak1/data");
let executable_location = binary_dir.join("extractor.exe");
let mut args = vec![
path_to_iso.clone(),
"--extract".to_string(),
"--validate".to_string(),
"--proj-path".to_string(),
data_folder.to_string_lossy().into_owned(),
];
if Path::new(&path_to_iso.clone()).is_dir() {
args.push("--folder".to_string());
}
let output = Command::new(&executable_location)
.args(args)
.current_dir(binary_dir)
.output()
.expect("failed to execute process");
Ok(())
}
}
}
#[tauri::command]
pub async fn run_decompiler(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
path_to_iso: String,
game_name: String,
) -> Result<(), ()> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
let install_path = Path::new(path);
let data_folder = install_path.join("active/jak1/data");
let mut source_path = path_to_iso;
if source_path.is_empty() {
// TODO - we could probably be more explicit here using a param
source_path = data_folder
.join("iso_data/jak1")
.to_string_lossy()
.to_string();
}
// TODO - be smarter
// TODO - make folder if it doesnt exist
// TODO - copy over the data folder
// TODO - log it to a file
let binary_dir = install_path.join("versions/official/v0.1.32/");
let executable_location = binary_dir.join("extractor.exe");
let output = Command::new(&executable_location)
.args([
source_path,
"--decompile".to_string(),
"--proj-path".to_string(),
data_folder.to_string_lossy().into_owned(),
])
.current_dir(binary_dir)
.output()
.expect("failed to execute process");
Ok(())
}
}
}
#[tauri::command]
pub async fn run_compiler(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
path_to_iso: String,
game_name: String,
) -> Result<(), ()> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
let install_path = Path::new(path);
let data_folder = install_path.join("active/jak1/data");
let mut source_path = path_to_iso;
if source_path.is_empty() {
// TODO - we could probably be more explicit here using a param
source_path = data_folder
.join("iso_data/jak1")
.to_string_lossy()
.to_string();
}
// TODO - be smarter
// TODO - make folder if it doesnt exist
// TODO - copy over the data folder
// TODO - log it to a file
let binary_dir = install_path.join("versions/official/v0.1.32/");
let executable_location = binary_dir.join("extractor.exe");
let output = Command::new(&executable_location)
.args([
source_path,
"--compile".to_string(),
"--proj-path".to_string(),
data_folder.to_string_lossy().into_owned(),
])
.current_dir(binary_dir)
.output()
.expect("failed to execute process");
Ok(())
}
}
}

View file

@ -1,57 +1,25 @@
use std::{path::Path, process::Command};
use std::path::Path;
use tauri::{api::path::config_dir, Manager};
use crate::config::LauncherConfig;
#[tauri::command]
pub async fn launch_game(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
in_debug: bool,
) -> Result<(), ()> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
// TODO - be smarter
// TODO - make folder if it doesnt exist
// TODO - copy over the data folder
// TODO - log it to a file
// TODO - check error code
let install_path = Path::new(path);
let binary_dir = install_path.join("versions/official/v0.1.32/");
let data_folder = install_path.join("active/jak1/data");
let executable_location = binary_dir.join("gk.exe");
let mut args = vec!["-boot".to_string(), "-fakeiso".to_string()];
// TODO - order unfortunately matters for gk args, this will be fixed eventually...
if in_debug {
args.push("-debug".to_string());
}
args.push("-proj-path".to_string());
args.push(data_folder.to_string_lossy().into_owned());
let output = Command::new(&executable_location)
.args(args)
.current_dir(binary_dir)
.spawn()
.expect("failed to execute process");
Ok(())
}
}
}
use super::CommandError;
#[tauri::command]
pub async fn uninstall_game(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
app_handle: tauri::AppHandle,
game_name: String,
) -> Result<(), ()> {
) -> Result<(), CommandError> {
let mut config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(()),
None => Err(CommandError::InvalidPath(format!(
"Can't uninstalled the game, no installation directory found"
))),
Some(path) => {
// TODO - cleanup
let data_folder = Path::new(path).join("active/jak1/data");
let data_folder = Path::new(path).join("active").join(&game_name).join("data");
std::fs::remove_dir_all(data_folder.join("decompiler_out"));
std::fs::remove_dir_all(data_folder.join("iso_data"));
std::fs::remove_dir_all(data_folder.join("out"));
@ -84,73 +52,3 @@ pub async fn reset_game_settings(game_name: String) -> Result<(), ()> {
}
}
}
// #[cfg(target_os = "windows")]
// #[tauri::command]
// pub async fn open_repl(proj_path: PathBuf, curr_dir: PathBuf) {
// tauri::async_runtime::spawn(async move {
// use std::process::Command as StdCommand;
// let repl = StdCommand::new("cmd.exe")
// .args([
// "/K",
// "start",
// "goalc",
// "--proj-path",
// proj_path.to_str().as_ref().unwrap(),
// ])
// .current_dir(curr_dir)
// .spawn()
// .unwrap();
// });
// }
// #[cfg(target_os = "linux")]
// #[tauri::command]
// pub async fn open_repl(proj_path: PathBuf, curr_dir: PathBuf) {
// tauri::async_runtime::spawn(async move {
// use tauri::api::process::Command;
// let tauri_cmd = Command::new_sidecar("goalc")
// .unwrap()
// .current_dir(curr_dir)
// .args(["--proj-path", proj_path.to_str().as_ref().unwrap()])
// .spawn();
// });
// }
#[tauri::command]
pub async fn open_repl(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
) -> Result<(), ()> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
// TODO - be smarter
// TODO - make folder if it doesnt exist
// TODO - copy over the data folder
// TODO - log it to a file
// TODO - check error code
// TODO - explore a linux option though this is very annoying because without doing a ton of research
// we seem to have to handle various terminals. Which honestly we should probably do on windows too
//
// So maybe we can make a menu where the user will specify what terminal to use / what launch-options to use
let install_path = Path::new(path);
let binary_dir = install_path.join("versions/official/v0.1.32/");
let data_folder = install_path.join("active/jak1/data");
let executable_location = binary_dir.join("goalc.exe");
let output = Command::new("cmd")
.args([
"/K",
"start",
"goalc.exe",
"--proj-path",
&data_folder.to_string_lossy().into_owned(),
])
.current_dir(binary_dir)
.spawn()
.expect("failed to execute process");
Ok(())
}
}
}

View file

@ -6,13 +6,15 @@ use std::{
use sysinfo::{CpuExt, DiskExt, System, SystemExt};
use zip::write::FileOptions;
use tauri::Manager;
use tauri::api::path::config_dir;
use crate::{
config::LauncherConfig,
util::zip::{append_dir_contents_to_zip, append_file_to_zip},
};
use super::CommandError;
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GPUInfo {
@ -58,138 +60,201 @@ pub struct SupportPackage {
pub disk_info: Vec<String>,
pub gpu_info: Vec<GPUInfo>,
pub game_info: PerGameInfo,
pub launcher_version: String,
}
#[tauri::command]
pub async fn generate_support_package(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
save_path: String,
) -> Result<(), ()> {
app_handle: tauri::AppHandle,
user_path: String,
) -> Result<(), CommandError> {
let mut package = SupportPackage::default();
let config_lock = config.lock().await;
// TODO - ask the user for the directory
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
// System Information
let mut system_info = System::new_all();
system_info.refresh_all();
package.total_memory_megabytes = system_info.total_memory() / 1024 / 1024;
package.cpu_name = system_info.cpus()[0].name().to_string();
package.cpu_vendor = system_info.cpus()[0].vendor_id().to_string();
package.cpu_brand = system_info.cpus()[0].brand().to_string();
package.os_name = system_info.os_version().unwrap_or("unknown".to_string());
package.os_name_long = system_info
.long_os_version()
.unwrap_or("unknown".to_string());
package.os_kernel_ver = system_info
.kernel_version()
.unwrap_or("unknown".to_string());
for disk in system_info.disks() {
package.disk_info.push(format!(
"{}:{}-{}GB/{}GB",
disk.mount_point().to_string_lossy(),
disk.name().to_string_lossy(),
disk.available_space() / 1024 / 1024 / 1024,
disk.total_space() / 1024 / 1024 / 1024
))
}
// TODO - consider adding a regex for all file appending so we skip weird files that weren't expected
// TODO - maybe long-term this can replace glewinfo / support vulkan?
let gpu_info_instance = wgpu::Instance::default();
for a in gpu_info_instance.enumerate_adapters(wgpu::Backends::all()) {
let info = a.get_info();
let mut gpu_info = GPUInfo::default();
gpu_info.name = info.name;
gpu_info.driver_name = info.driver;
gpu_info.driver_info = info.driver_info;
package.gpu_info.push(gpu_info);
}
// Create zip file
let save_path = Path::new(&path.clone()).join("support-package.zip");
let save_file = std::fs::File::create(save_path).expect("TODO");
let mut zip_file = zip::ZipWriter::new(save_file);
// Save OpenGOAL config folder (this includes saves and settings)
let game_config_dir = Path::new("C:\\Users\\xtvas\\AppData\\Roaming\\OpenGOAL");
append_dir_contents_to_zip(&mut zip_file, &game_config_dir, "Game Settings and Saves")
.expect("good");
// Save Launcher config folder
// TODO - prompt on first startup to delete data folder
let launcher_logs = Path::new("C:\\Users\\xtvas\\AppData\\Roaming\\OpenGOAL-Launcher");
append_dir_contents_to_zip(
&mut zip_file,
&launcher_logs.join("logs"),
"Launcher Settings and Logs/logs",
)
.expect("good");
append_file_to_zip(
&mut zip_file,
&launcher_logs.join("settings.json"),
"Launcher Settings and Logs/settings.json",
)
.expect("TODO");
// Save Logs
// TODO - for all games
let jak1_log_dir = Path::new("C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data\\log");
append_dir_contents_to_zip(&mut zip_file, &jak1_log_dir, "Game Logs and ISO Info/Jak 1")
.expect("TODO");
// Per Game Info
let texture_repl_dir =
Path::new("C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data\\texture_replacements");
package.game_info.jak1.has_texture_packs =
texture_repl_dir.exists() && !texture_repl_dir.read_dir().unwrap().next().is_none();
let build_info_path = Path::new(
"C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data\\iso_data\\jak1\\buildinfo.json",
);
append_file_to_zip(
&mut zip_file,
&build_info_path,
"Game Logs and ISO Info/Jak 1/buildinfo.json",
)
.expect("TODO");
let data_dir = Path::new("C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data");
let version_data_dir =
Path::new("C:\\Users\\xtvas\\Downloads\\yee\\versions\\official\\v0.1.32\\data");
package
.game_info
.jak1
.release_integrity
.decompiler_folder_modified = dir_diff::is_different(
data_dir.join("decompiler"),
version_data_dir.join("decompiler"),
)
.unwrap();
package
.game_info
.jak1
.release_integrity
.game_folder_modified =
dir_diff::is_different(data_dir.join("game"), version_data_dir.join("game")).unwrap();
package.game_info.jak1.release_integrity.goal_src_modified =
dir_diff::is_different(data_dir.join("goal_src"), version_data_dir.join("goal_src"))
.unwrap();
// Dump High Level Info
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::DEFLATE)
.unix_permissions(0o755);
zip_file.start_file("support-info.json", options).unwrap();
let mut json_buffer = Vec::new();
let json_writer = BufWriter::new(&mut json_buffer);
serde_json::to_writer_pretty(json_writer, &package).expect("TODO");
zip_file.write_all(&json_buffer).expect("TODO");
zip_file.finish().expect("TODO");
Ok(())
let install_path = match &config_lock.installation_dir {
None => {
return Err(CommandError::Support(format!(
"No installation directory set, can't generate the support package"
)))
}
Some(path) => Path::new(path),
};
// System Information
let mut system_info = System::new_all();
system_info.refresh_all();
package.total_memory_megabytes = system_info.total_memory() / 1024 / 1024;
package.cpu_name = system_info.cpus()[0].name().to_string();
package.cpu_vendor = system_info.cpus()[0].vendor_id().to_string();
package.cpu_brand = system_info.cpus()[0].brand().to_string();
package.os_name = system_info.os_version().unwrap_or("unknown".to_string());
package.os_name_long = system_info
.long_os_version()
.unwrap_or("unknown".to_string());
package.os_kernel_ver = system_info
.kernel_version()
.unwrap_or("unknown".to_string());
package.launcher_version = app_handle.package_info().version.to_string();
for disk in system_info.disks() {
package.disk_info.push(format!(
"{}:{}-{}GB/{}GB",
disk.mount_point().to_string_lossy(),
disk.name().to_string_lossy(),
disk.available_space() / 1024 / 1024 / 1024,
disk.total_space() / 1024 / 1024 / 1024
))
}
// TODO - maybe long-term this can replace glewinfo / support vulkan?
let gpu_info_instance = wgpu::Instance::default();
for a in gpu_info_instance.enumerate_adapters(wgpu::Backends::all()) {
let info = a.get_info();
let mut gpu_info = GPUInfo::default();
gpu_info.name = info.name;
gpu_info.driver_name = info.driver;
gpu_info.driver_info = info.driver_info;
package.gpu_info.push(gpu_info);
}
// Create zip file
let save_path = Path::new(&user_path).join("support-package.zip");
let save_file = std::fs::File::create(save_path)
.map_err(|_| CommandError::Support(format!("Unable to create support file")))?;
let mut zip_file = zip::ZipWriter::new(save_file);
// Save OpenGOAL config folder (this includes saves and settings)
let game_config_dir = match config_dir() {
None => {
return Err(CommandError::Support(format!(
"Couldn't determine application config directory"
)))
}
Some(path) => path.join("OpenGOAL"),
};
append_dir_contents_to_zip(&mut zip_file, &game_config_dir, "Game Settings and Saves").map_err(
|_| {
CommandError::Support(format!(
"Unable to append game settings and saves to the support package"
))
},
)?;
// TODO - don't fail fast so eagerly (when a path isn't found just continue on)
// Save Launcher config folder
// TODO - prompt on first startup to delete data folder
let launcher_config_dir = match app_handle.path_resolver().app_config_dir() {
None => {
return Err(CommandError::Support(format!(
"Couldn't determine launcher config directory"
)))
}
Some(path) => path,
};
append_dir_contents_to_zip(
&mut zip_file,
&launcher_config_dir.join("logs"),
"Launcher Settings and Logs/logs",
)
.map_err(|_| {
CommandError::Support(format!(
"Unable to append launcher logs to the support package"
))
})?;
append_file_to_zip(
&mut zip_file,
&launcher_config_dir.join("settings.json"),
"Launcher Settings and Logs/settings.json",
)
.map_err(|_| {
CommandError::Support(format!(
"Unable to append launcher settings to the support package"
))
})?;
// Save Logs
let active_version_dir = install_path.join("active");
// TODO - for all games
let jak1_log_dir = active_version_dir.join("jak1").join("data").join("log");
append_dir_contents_to_zip(&mut zip_file, &jak1_log_dir, "Game Logs and ISO Info/Jak 1")
.map_err(|_| CommandError::Support(format!("Unable to append game logs to support package")))?;
// Per Game Info
let texture_repl_dir = active_version_dir
.join("jak1")
.join("data")
.join("texture_replacements");
package.game_info.jak1.has_texture_packs =
texture_repl_dir.exists() && !texture_repl_dir.read_dir().unwrap().next().is_none();
let build_info_path = active_version_dir
.join("jak1")
.join("data")
.join("iso_data")
.join("jak1")
.join("buildinfo.json");
append_file_to_zip(
&mut zip_file,
&build_info_path,
"Game Logs and ISO Info/Jak 1/buildinfo.json",
)
.map_err(|_| {
CommandError::Support(format!("Unable to append iso metadata to support package"))
})?;
if config_lock.active_version_folder.is_some() && config_lock.active_version_folder.is_some() {
let data_dir = active_version_dir.join("jak1").join("data");
let version_data_dir = install_path
.join("versions")
.join(config_lock.active_version_folder.as_ref().unwrap())
.join(config_lock.active_version.as_ref().unwrap())
.join("data");
package
.game_info
.jak1
.release_integrity
.decompiler_folder_modified = dir_diff::is_different(
data_dir.join("decompiler"),
version_data_dir.join("decompiler"),
)
.unwrap_or(true);
package
.game_info
.jak1
.release_integrity
.game_folder_modified =
dir_diff::is_different(data_dir.join("game"), version_data_dir.join("game")).unwrap_or(true);
package.game_info.jak1.release_integrity.goal_src_modified =
dir_diff::is_different(data_dir.join("goal_src"), version_data_dir.join("goal_src"))
.unwrap_or(true);
}
// Dump High Level Info
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::DEFLATE)
.unix_permissions(0o755);
zip_file
.start_file("support-info.json", options)
.map_err(|_| {
CommandError::Support(format!(
"Create high level support info entry in support package"
))
})?;
let mut json_buffer = Vec::new();
let json_writer = BufWriter::new(&mut json_buffer);
serde_json::to_writer_pretty(json_writer, &package).map_err(|_| {
CommandError::Support(format!(
"Unable to write high-level support info to the support package"
))
})?;
zip_file.write_all(&json_buffer).map_err(|_| {
CommandError::Support(format!(
"Unable to write high-level support info to the support package"
))
})?;
zip_file
.finish()
.map_err(|_| CommandError::Support(format!("Unable to finalize zip file")))?;
Ok(())
}

View file

@ -1,132 +1,144 @@
use futures_util::StreamExt;
use std::{io::Cursor, path::Path};
use tauri::Manager;
use tokio::{fs::File, io::AsyncWriteExt};
use std::path::Path;
use crate::{config::LauncherConfig, util};
use crate::{
config::LauncherConfig,
util::{
file::{create_dir, delete_dir_or_folder},
network::download_file,
os::open_dir_in_os,
zip::extract_and_delete_zip_file,
},
};
use super::CommandError;
#[tauri::command]
pub async fn list_downloaded_versions(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
version_folder: String,
) -> Result<Vec<String>, ()> {
) -> Result<Vec<String>, CommandError> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(Vec::new()),
Some(path) => {
let expected_path = Path::new(path).join("versions").join(version_folder);
if !expected_path.is_dir() {
Ok(Vec::new())
} else {
match std::fs::read_dir(expected_path) {
Err(_) => Ok(Vec::new()),
Ok(entries) => Ok(
entries
.filter_map(|e| {
e.ok().and_then(|d| {
let p = d.path();
if p.is_dir() {
Some(
p.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or("".into()),
)
} else {
None
}
})
})
.collect(),
),
}
}
}
let install_path = match &config_lock.installation_dir {
None => return Ok(Vec::new()),
Some(path) => Path::new(path),
};
let expected_path = Path::new(install_path)
.join("versions")
.join(version_folder);
if !expected_path.exists() || !expected_path.is_dir() {
log::info!(
"No {} folder found, returning no releases",
expected_path.display()
);
return Ok(Vec::new());
}
let entries = std::fs::read_dir(&expected_path).map_err(|_| {
CommandError::VersionManagement(format!(
"Unable to read versions from {}",
expected_path.display()
))
})?;
Ok(
entries
.filter_map(|e| {
e.ok().and_then(|d| {
let p = d.path();
if p.is_dir() {
Some(
p.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or("".into()),
)
} else {
None
}
})
})
.collect(),
)
}
#[tauri::command]
pub async fn download_official_version(
pub async fn download_version(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
version: String,
version_folder: String,
url: String,
) -> Result<(), bool> {
) -> Result<(), CommandError> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
// TODO - severe lack of safety here!
// TODO - make the dir and the file name
let expected_path = Path::new(&path).join("versions/official/test.zip");
let client = reqwest::Client::new();
let mut req = client.get(url);
let res = req.send().await.expect("");
let total = res.content_length().expect("");
let mut file = File::create(expected_path).await.expect("");
let mut stream = res.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.expect("");
file.write_all(&chunk).await.expect("");
}
let target_dir = Path::new(&path).join("versions/official/").join(version);
let zip_path = Path::new(&path).join("versions/official/test.zip");
let archive: Vec<u8> = std::fs::read(&zip_path.clone()).unwrap();
zip_extract::extract(Cursor::new(archive), &target_dir, true).expect("");
std::fs::remove_file(zip_path).expect("TODO");
Ok(())
let install_path = match &config_lock.installation_dir {
None => {
return Err(CommandError::VersionManagement(format!(
"Cannot install version, no installation directory set"
)))
}
}
Some(path) => Path::new(path),
};
let dest_dir = install_path
.join("versions")
.join(&version_folder)
.join(&version);
// Delete the directory if it exists, and create it from scratch
delete_dir_or_folder(&dest_dir).map_err(|_| {
CommandError::VersionManagement(format!(
"Unable to prepare destination folder '{}' for download",
dest_dir.display()
))
})?;
create_dir(&dest_dir).map_err(|_| {
CommandError::VersionManagement(format!(
"Unable to prepare destination folder '{}' for download",
dest_dir.display()
))
})?;
let download_path = install_path
.join("versions")
.join(version_folder)
.join(format!("{}.zip", version));
// Download the file
download_file(&url, &download_path).await.map_err(|_| {
CommandError::VersionManagement(format!("Unable to successfully download version"))
})?;
// Extract the zip file
extract_and_delete_zip_file(&download_path, &dest_dir).map_err(|_| {
CommandError::VersionManagement(format!("Unable to successfully extract downloaded version"))
})?;
Ok(())
}
#[tauri::command]
pub async fn go_to_version_folder(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
version_folder: String,
) -> Result<(), ()> {
) -> Result<(), CommandError> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Err(()),
Some(path) => {
let expected_path = Path::new(path).join("versions").join(version_folder);
util::open_dir_in_os(expected_path.to_string_lossy().into_owned());
Ok(())
let install_path = match &config_lock.installation_dir {
None => {
return Err(CommandError::VersionManagement(format!(
"Cannot go to version folder, no installation directory set"
)))
}
}
}
Some(path) => Path::new(path),
};
#[tauri::command]
pub async fn save_active_version_change(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
app_handle: tauri::AppHandle,
version_folder: String,
new_active_version: String,
) -> Result<(), ()> {
let mut config_lock = config.lock().await;
// TODO - error checking
config_lock.set_active_version_folder(version_folder);
config_lock.set_active_version(new_active_version);
app_handle.emit_all("toolingVersionChanged", {}).unwrap();
let folder_path = Path::new(install_path)
.join("versions")
.join(version_folder);
create_dir(&folder_path).map_err(|_| {
CommandError::VersionManagement(format!(
"Unable to go to create version folder '{}' in order to open it",
folder_path.display()
))
})?;
open_dir_in_os(folder_path.to_string_lossy().into_owned())
.map_err(|_| CommandError::VersionManagement(format!("Unable to go to open folder in OS")))?;
Ok(())
}
#[tauri::command]
pub async fn get_active_version(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<String>, ()> {
let config_lock = config.lock().await;
Ok(config_lock.active_version.clone())
}
#[tauri::command]
pub async fn get_active_version_folder(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<String>, ()> {
let config_lock = config.lock().await;
Ok(config_lock.active_version_folder.clone())
}

View file

@ -0,0 +1,11 @@
use tauri::Manager;
#[tauri::command]
pub async fn close_splashscreen(window: tauri::Window) {
// Close splashscreen
if let Some(splashscreen) = window.get_window("splashscreen") {
splashscreen.close().unwrap();
}
// Show main window
window.get_window("main").unwrap().show().unwrap();
}

View file

@ -11,9 +11,18 @@
use std::{fs, path::PathBuf};
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
JSONError(#[from] serde_json::Error),
#[error("{0}")]
Configuration(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub enum SupportedGame {
JAK1,
@ -171,38 +180,57 @@ impl LauncherConfig {
}
}
pub fn save_config(&self) {
match &self.settings_path {
pub fn save_config(&self) -> Result<(), ConfigError> {
let settings_path = match &self.settings_path {
None => {
log::warn!("Can't save the settings file, as no path was initialized!");
return Err(ConfigError::Configuration(format!(
"No settings path defined, unable to save settings!"
)));
}
Some(path) => {
let file = fs::File::create(path).expect("TODO");
serde_json::to_writer_pretty(file, &self);
}
}
Some(path) => path,
};
let file = fs::File::create(settings_path)?;
serde_json::to_writer_pretty(file, &self)?;
Ok(())
}
pub fn set_install_directory(&mut self, new_dir: String) {
pub fn set_install_directory(&mut self, new_dir: String) -> Result<(), ConfigError> {
self.installation_dir = Some(new_dir);
self.save_config();
self.save_config()?;
Ok(())
}
pub fn set_active_version(&mut self, new_version: String) {
pub fn set_opengl_requirement_met(&mut self, new_val: bool) -> Result<(), ConfigError> {
self.requirements.opengl = Some(new_val);
self.save_config()?;
Ok(())
}
pub fn set_active_version(&mut self, new_version: String) -> Result<(), ConfigError> {
self.active_version = Some(new_version);
self.save_config();
self.save_config()?;
Ok(())
}
pub fn set_active_version_folder(&mut self, new_version_folder: String) {
pub fn set_active_version_folder(
&mut self,
new_version_folder: String,
) -> Result<(), ConfigError> {
self.active_version_folder = Some(new_version_folder);
self.save_config();
self.save_config()?;
Ok(())
}
// TODO - this pattern isn't great. It's made worse by trying to be backwards compatible though
// with the old format
// TODO - this pattern isn't great. It's made awkward by trying to be backwards compatible
// with the old format though
//
// I think there should be an enum involved here somewhere/somehow
pub fn update_installed_game_version(&mut self, game_name: String, installed: bool) {
pub fn update_installed_game_version(
&mut self,
game_name: String,
installed: bool,
) -> Result<(), ConfigError> {
match game_name.as_str() {
"jak1" => {
self.games.jak1.is_installed = installed;
@ -246,7 +274,8 @@ impl LauncherConfig {
}
_ => {}
}
self.save_config();
self.save_config()?;
Ok(())
}
pub fn is_game_installed(&self, game_name: String) -> bool {

View file

@ -11,10 +11,6 @@ mod commands;
mod config;
mod textures;
mod util;
use commands::{close_splashscreen, copy_dir, get_highest_simd, open_dir};
use textures::{extract_textures, get_all_texture_packs};
pub type FFIResult<T> = Result<T, String>;
fn main() {
// TODO - switch to https://github.com/daboross/fern so we can setup easy logging
@ -40,35 +36,30 @@ fn main() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::config::get_install_directory,
commands::config::set_install_directory,
commands::config::is_avx_requirement_met,
commands::config::is_opengl_requirement_met,
commands::binaries::extract_and_validate_iso,
commands::binaries::launch_game,
commands::binaries::open_repl,
commands::binaries::run_compiler,
commands::binaries::run_decompiler,
commands::config::finalize_installation,
commands::config::is_game_installed,
commands::config::get_installed_version,
commands::config::get_active_version_folder,
commands::config::get_active_version,
commands::config::get_install_directory,
commands::config::get_installed_version_folder,
commands::versions::list_downloaded_versions,
commands::versions::download_official_version,
commands::versions::go_to_version_folder,
commands::versions::save_active_version_change,
commands::versions::get_active_version,
commands::versions::get_active_version_folder,
commands::extractor::extract_and_validate_iso,
commands::extractor::run_decompiler,
commands::extractor::run_compiler,
commands::game::launch_game,
commands::game::uninstall_game,
commands::config::get_installed_version,
commands::config::is_avx_requirement_met,
commands::config::is_avx_supported,
commands::config::is_game_installed,
commands::config::is_opengl_requirement_met,
commands::config::save_active_version_change,
commands::config::set_install_directory,
commands::game::reset_game_settings,
commands::game::open_repl,
commands::game::uninstall_game,
commands::support::generate_support_package,
// Requirements Checking
get_highest_simd,
open_dir,
copy_dir,
close_splashscreen,
extract_textures,
get_all_texture_packs
commands::versions::download_version,
commands::versions::go_to_version_folder,
commands::versions::list_downloaded_versions,
commands::window::close_splashscreen
])
.build(tauri::generate_context!())
.expect("error building tauri app")

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::{
fs,
io::{self, Cursor, Read},
io::{self, Read},
path::{Path, PathBuf},
};

View file

@ -1,17 +1,4 @@
use std::process::Command;
pub mod file;
pub mod network;
pub mod os;
pub mod zip;
#[cfg(target_os = "windows")]
const FILE_OPENING_PROGRAM: &str = "explorer";
#[cfg(target_os = "linux")]
const FILE_OPENING_PROGRAM: &str = "explorer";
#[cfg(target_os = "macos")]
const FILE_OPENING_PROGRAM: &str = "explorer";
pub fn open_dir_in_os(dir: String) {
Command::new(FILE_OPENING_PROGRAM)
.arg(dir) // <- Specify the directory you'd like to open.
.spawn()
.unwrap();
}

View file

@ -0,0 +1,20 @@
use std::path::PathBuf;
pub fn delete_dir_or_folder(path: &PathBuf) -> Result<(), std::io::Error> {
if path.exists() {
if path.is_dir() {
std::fs::remove_dir_all(path)?;
} else {
std::fs::remove_file(path)?;
}
}
Ok(())
}
pub fn create_dir(path: &PathBuf) -> Result<(), std::io::Error> {
if path.exists() {
return Ok(());
}
std::fs::create_dir_all(path)?;
Ok(())
}

View file

@ -0,0 +1,28 @@
use futures_util::StreamExt;
use std::path::PathBuf;
use tokio::{fs::File, io::AsyncWriteExt};
#[derive(Debug, thiserror::Error)]
pub enum NetworkError {
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
NetworkRequest(#[from] reqwest::Error),
#[error("{0}")]
Other(String),
}
pub async fn download_file(url: &String, destination: &PathBuf) -> Result<(), NetworkError> {
let client = reqwest::Client::new();
let req = client.get(url);
let res = req.send().await?;
let mut file = File::create(destination).await?;
let mut stream = res.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
}
Ok(())
}

13
src-tauri/src/util/os.rs Normal file
View file

@ -0,0 +1,13 @@
use std::process::Command;
#[cfg(target_os = "windows")]
const FILE_OPENING_PROGRAM: &str = "explorer";
#[cfg(target_os = "linux")]
const FILE_OPENING_PROGRAM: &str = "xdg-open";
#[cfg(target_os = "macos")]
const FILE_OPENING_PROGRAM: &str = "open";
pub fn open_dir_in_os(dir: String) -> Result<(), std::io::Error> {
Command::new(FILE_OPENING_PROGRAM).arg(dir).spawn()?;
Ok(())
}

View file

@ -1,9 +1,10 @@
use std::io::Cursor;
use std::path::PathBuf;
use std::{
fs::File,
io::{Read, Write},
path::Path,
};
use walkdir::WalkDir;
use zip::write::FileOptions;
@ -13,7 +14,7 @@ pub fn append_dir_contents_to_zip(
internal_folder: &str,
) -> zip::result::ZipResult<()> {
if !dir.exists() {
Result::<(), ()>::Err(());
return Result::Ok(());
}
let iter = WalkDir::new(dir).into_iter().filter_map(|e| e.ok());
@ -76,3 +77,13 @@ pub fn append_file_to_zip(
Result::Ok(())
}
pub fn extract_and_delete_zip_file(
zip_path: &PathBuf,
extract_dir: &PathBuf,
) -> Result<(), zip_extract::ZipExtractError> {
let archive: Vec<u8> = std::fs::read(zip_path)?;
zip_extract::extract(Cursor::new(archive), extract_dir, true)?;
std::fs::remove_file(zip_path)?;
Result::Ok(())
}

View file

@ -54,6 +54,7 @@
progressTracker.proceed();
progressTracker.proceed();
} else if (jobType === "updateGame") {
// TODO - update data dir
progressTracker.init([
{
status: "queued",

View file

@ -36,7 +36,7 @@
</ul>
<p class="mb-3">
You can either update the game to this new version (no save data will be
lost) or you can change your active version to match
lost) or you can rollback your active version to match
</p>
<div
class="justify-center items-center space-y-4 sm:flex sm:space-y-0 sm:space-x-4"