diff --git a/theseus/src/api/jre.rs b/theseus/src/api/jre.rs index 3a2d43a6..3624859d 100644 --- a/theseus/src/api/jre.rs +++ b/theseus/src/api/jre.rs @@ -112,7 +112,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result { ) .await?; - let path = state.directories.java_versions_dir(); + let path = state.directories.java_versions_dir().await; if path.exists() { io::remove_dir_all(&path).await?; diff --git a/theseus/src/api/logs.rs b/theseus/src/api/logs.rs index b1b49ace..2e3420ee 100644 --- a/theseus/src/api/logs.rs +++ b/theseus/src/api/logs.rs @@ -1,6 +1,6 @@ use crate::{ util::io::{self, IOError}, - State, + {state::ProfilePathId, State}, }; use serde::{Deserialize, Serialize}; @@ -11,7 +11,7 @@ pub struct Logs { } impl Logs { async fn build( - profile_uuid: uuid::Uuid, + profile_subpath: &ProfilePathId, datetime_string: String, clear_contents: Option, ) -> crate::Result { @@ -20,7 +20,7 @@ impl Logs { None } else { Some( - get_output_by_datetime(profile_uuid, &datetime_string) + get_output_by_datetime(profile_subpath, &datetime_string) .await?, ) }, @@ -35,7 +35,18 @@ pub async fn get_logs( clear_contents: Option, ) -> crate::Result> { let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let profile_path = if let Some(p) = + crate::profile::get_by_uuid(profile_uuid, None).await? + { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_uuid.to_string(), + ) + .into()); + }; + + let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; let mut logs = Vec::new(); if logs_folder.exists() { for entry in std::fs::read_dir(&logs_folder) @@ -48,7 +59,7 @@ pub async fn get_logs( if let Some(datetime_string) = path.file_name() { logs.push( Logs::build( - profile_uuid, + &profile_path, datetime_string.to_string_lossy().to_string(), clear_contents, ) @@ -69,9 +80,19 @@ pub async fn get_logs_by_datetime( profile_uuid: uuid::Uuid, datetime_string: String, ) -> crate::Result { + let profile_path = if let Some(p) = + crate::profile::get_by_uuid(profile_uuid, None).await? + { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_uuid.to_string(), + ) + .into()); + }; Ok(Logs { output: Some( - get_output_by_datetime(profile_uuid, &datetime_string).await?, + get_output_by_datetime(&profile_path, &datetime_string).await?, ), datetime_string, }) @@ -79,19 +100,31 @@ pub async fn get_logs_by_datetime( #[tracing::instrument] pub async fn get_output_by_datetime( - profile_uuid: uuid::Uuid, + profile_subpath: &ProfilePathId, datetime_string: &str, ) -> crate::Result { let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let logs_folder = + state.directories.profile_logs_dir(profile_subpath).await?; let path = logs_folder.join(datetime_string).join("stdout.log"); Ok(io::read_to_string(&path).await?) } #[tracing::instrument] pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> { + let profile_path = if let Some(p) = + crate::profile::get_by_uuid(profile_uuid, None).await? + { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_uuid.to_string(), + ) + .into()); + }; + let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; for entry in std::fs::read_dir(&logs_folder) .map_err(|e| IOError::with_path(e, &logs_folder))? { @@ -109,8 +142,19 @@ pub async fn delete_logs_by_datetime( profile_uuid: uuid::Uuid, datetime_string: &str, ) -> crate::Result<()> { + let profile_path = if let Some(p) = + crate::profile::get_by_uuid(profile_uuid, None).await? + { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_uuid.to_string(), + ) + .into()); + }; + let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; let path = logs_folder.join(datetime_string); io::remove_dir_all(&path).await?; Ok(()) diff --git a/theseus/src/api/mod.rs b/theseus/src/api/mod.rs index cd0277d3..f58a538a 100644 --- a/theseus/src/api/mod.rs +++ b/theseus/src/api/mod.rs @@ -29,6 +29,7 @@ pub mod prelude { profile::{self, Profile}, profile_create, settings, state::JavaGlobals, + state::{ProfilePathId, ProjectPathId}, util::{ io::{canonicalize, IOError}, jre::JavaVersion, diff --git a/theseus/src/api/pack/install.rs b/theseus/src/api/pack/install.rs index 097e2ea9..47088190 100644 --- a/theseus/src/api/pack/install.rs +++ b/theseus/src/api/pack/install.rs @@ -4,7 +4,7 @@ use crate::event::emit::{ }; use crate::event::LoadingBarType; use crate::pack::install_from::{EnvType, PackFile, PackFileHash}; -use crate::state::{LinkedData, ProfileInstallStage, SideType}; +use crate::state::{LinkedData, ProfileInstallStage, ProfilePathId, SideType}; use crate::util::fetch::{fetch_mirrors, write}; use crate::State; use async_zip::tokio::read::seek::ZipFileReader; @@ -20,8 +20,8 @@ use super::install_from::{ #[theseus_macros::debug_pin] pub async fn install_pack( location: CreatePackLocation, - profile: PathBuf, -) -> crate::Result { + profile_path: ProfilePathId, +) -> crate::Result { // Get file from description let description: CreatePackDescription = match location { CreatePackLocation::FromVersionId { @@ -31,12 +31,16 @@ pub async fn install_pack( icon_url, } => { generate_pack_from_version_id( - project_id, version_id, title, icon_url, profile, + project_id, + version_id, + title, + icon_url, + profile_path, ) .await? } CreatePackLocation::FromFile { path } => { - generate_pack_from_file(path, profile).await? + generate_pack_from_file(path, profile_path).await? } }; @@ -46,7 +50,7 @@ pub async fn install_pack( let project_id = description.project_id; let version_id = description.version_id; let existing_loading_bar = description.existing_loading_bar; - let profile = description.profile; + let profile_path = description.profile_path; let state = &State::get().await?; @@ -125,7 +129,7 @@ pub async fn install_pack( loader_version.cloned(), ) .await?; - crate::api::profile::edit(&profile, |prof| { + crate::api::profile::edit(&profile_path, |prof| { prof.metadata.name = override_title.clone().unwrap_or_else(|| pack.name.clone()); prof.install_stage = ProfileInstallStage::PackInstalling; @@ -142,12 +146,15 @@ pub async fn install_pack( }) .await?; - let profile = profile.clone(); + let profile_path = profile_path.clone(); let result = async { let loading_bar = init_or_edit_loading( existing_loading_bar, LoadingBarType::PackDownload { - profile_path: profile.clone(), + profile_path: profile_path + .get_full_path() + .await? + .clone(), pack_name: pack.name.clone(), icon, pack_id: project_id, @@ -169,7 +176,7 @@ pub async fn install_pack( num_files, None, |project| { - let profile = profile.clone(); + let profile_path = profile_path.clone(); async move { //TODO: Future update: prompt user for optional files in a modpack if let Some(env) = project.env { @@ -203,7 +210,10 @@ pub async fn install_pack( match path { Component::CurDir | Component::Normal(_) => { - let path = profile.join(project.path); + let path = profile_path + .get_full_path() + .await? + .join(project.path); write( &path, &file, @@ -265,7 +275,10 @@ pub async fn install_pack( if new_path.file_name().is_some() { write( - &profile.join(new_path), + &profile_path + .get_full_path() + .await? + .join(new_path), &content, &state.io_semaphore, ) @@ -285,7 +298,7 @@ pub async fn install_pack( } if let Some(profile_val) = - crate::api::profile::get(&profile, None).await? + crate::api::profile::get(&profile_path, None).await? { crate::launcher::install_minecraft( &profile_val, @@ -296,14 +309,14 @@ pub async fn install_pack( State::sync().await?; } - Ok::(profile.clone()) + Ok::(profile_path.clone()) } .await; match result { Ok(profile) => Ok(profile), Err(err) => { - let _ = crate::api::profile::remove(&profile).await; + let _ = crate::api::profile::remove(&profile_path).await; Err(err) } @@ -319,7 +332,7 @@ pub async fn install_pack( match result { Ok(profile) => Ok(profile), Err(err) => { - let _ = crate::api::profile::remove(&profile).await; + let _ = crate::api::profile::remove(&profile_path).await; Err(err) } diff --git a/theseus/src/api/pack/install_from.rs b/theseus/src/api/pack/install_from.rs index dff8ec2c..2e92dc16 100644 --- a/theseus/src/api/pack/install_from.rs +++ b/theseus/src/api/pack/install_from.rs @@ -2,7 +2,9 @@ use crate::config::MODRINTH_API_URL; use crate::data::ModLoader; use crate::event::emit::{emit_loading, init_loading}; use crate::event::{LoadingBarId, LoadingBarType}; -use crate::state::{LinkedData, ModrinthProject, ModrinthVersion, SideType}; +use crate::state::{ + LinkedData, ModrinthProject, ModrinthVersion, ProfilePathId, SideType, +}; use crate::util::fetch::{ fetch, fetch_advanced, fetch_json, write_cached_icon, }; @@ -71,7 +73,7 @@ pub enum PackDependency { Minecraft, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", tag = "type")] pub enum CreatePackLocation { FromVersionId { @@ -98,6 +100,7 @@ pub struct CreatePackProfile { pub skip_install_profile: Option, } +#[derive(Debug)] pub struct CreatePackDescription { pub file: bytes::Bytes, pub icon: Option, @@ -105,7 +108,7 @@ pub struct CreatePackDescription { pub project_id: Option, pub version_id: Option, pub existing_loading_bar: Option, - pub profile: PathBuf, + pub profile_path: ProfilePathId, } pub fn get_profile_from_pack( @@ -158,13 +161,13 @@ pub async fn generate_pack_from_version_id( version_id: String, title: String, icon_url: Option, - profile: PathBuf, + profile_path: ProfilePathId, ) -> crate::Result { let state = State::get().await?; let loading_bar = init_loading( LoadingBarType::PackFileDownload { - profile_path: profile.clone(), + profile_path: profile_path.get_full_path().await?, pack_name: title, icon: icon_url, pack_version: version_id.clone(), @@ -253,7 +256,7 @@ pub async fn generate_pack_from_version_id( project_id: Some(project_id), version_id: Some(version_id), existing_loading_bar: Some(loading_bar), - profile, + profile_path, }) } @@ -261,7 +264,7 @@ pub async fn generate_pack_from_version_id( #[theseus_macros::debug_pin] pub async fn generate_pack_from_file( path: PathBuf, - profile: PathBuf, + profile_path: ProfilePathId, ) -> crate::Result { let file = io::read(&path).await?; Ok(CreatePackDescription { @@ -271,6 +274,6 @@ pub async fn generate_pack_from_file( project_id: None, version_id: None, existing_loading_bar: None, - profile, + profile_path, }) } diff --git a/theseus/src/api/process.rs b/theseus/src/api/process.rs index 7673c167..c9a86293 100644 --- a/theseus/src/api/process.rs +++ b/theseus/src/api/process.rs @@ -1,15 +1,17 @@ //! Theseus process management interface -use std::path::{Path, PathBuf}; use uuid::Uuid; -use crate::{state::MinecraftChild, util::io::IOError}; pub use crate::{ state::{ Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, }, State, }; +use crate::{ + state::{MinecraftChild, ProfilePathId}, + util::io::IOError, +}; // Gets whether a child process stored in the state by UUID has finished #[tracing::instrument] @@ -45,7 +47,8 @@ pub async fn get_all_running_uuids() -> crate::Result> { // Gets the Profile paths of each *running* stored process in the state #[tracing::instrument] -pub async fn get_all_running_profile_paths() -> crate::Result> { +pub async fn get_all_running_profile_paths() -> crate::Result> +{ let state = State::get().await?; let children = state.children.read().await; children.running_profile_paths().await @@ -62,7 +65,7 @@ pub async fn get_all_running_profiles() -> crate::Result> { // Gets the UUID of each stored process in the state by profile path #[tracing::instrument] pub async fn get_uuids_by_profile_path( - profile_path: &Path, + profile_path: ProfilePathId, ) -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index 1657cf05..fd11085f 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -7,7 +7,7 @@ use crate::pack::install_from::{ EnvType, PackDependency, PackFile, PackFileHash, PackFormat, }; use crate::prelude::JavaVersion; -use crate::state::ProjectMetadata; +use crate::state::{ProfilePathId, ProjectMetadata, ProjectPathId}; use crate::util::io::{self, IOError}; use crate::{ @@ -32,14 +32,14 @@ use tokio::{fs::File, process::Command, sync::RwLock}; /// Remove a profile #[tracing::instrument] -pub async fn remove(path: &Path) -> crate::Result<()> { +pub async fn remove(path: &ProfilePathId) -> crate::Result<()> { let state = State::get().await?; let mut profiles = state.profiles.write().await; if let Some(profile) = profiles.remove(path).await? { emit_profile( profile.uuid, - profile.path.clone(), + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Removed, ) @@ -49,13 +49,14 @@ pub async fn remove(path: &Path) -> crate::Result<()> { Ok(()) } -/// Get a profile by path, +/// Get a profile by relative path (or, name) #[tracing::instrument] pub async fn get( - path: &Path, + path: &ProfilePathId, clear_projects: Option, ) -> crate::Result> { let state = State::get().await?; + let profiles = state.profiles.read().await; let mut profile = profiles.0.get(path).cloned(); @@ -68,9 +69,29 @@ pub async fn get( Ok(profile) } +/// Get a profile by uuid +#[tracing::instrument] +pub async fn get_by_uuid( + uuid: uuid::Uuid, + clear_projects: Option, +) -> crate::Result> { + let state = State::get().await?; + + let profiles = state.profiles.read().await; + let mut profile = profiles.0.values().find(|x| x.uuid == uuid).cloned(); + + if clear_projects.unwrap_or(false) { + if let Some(profile) = &mut profile { + profile.projects = HashMap::new(); + } + } + + Ok(profile) +} + /// Edit a profile using a given asynchronous closure pub async fn edit( - path: &Path, + path: &ProfilePathId, action: impl Fn(&mut Profile) -> Fut, ) -> crate::Result<()> where @@ -85,7 +106,7 @@ where emit_profile( profile.uuid, - profile.path.clone(), + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Edited, ) @@ -93,16 +114,14 @@ where Ok(()) } - None => Err(crate::ErrorKind::UnmanagedProfileError( - path.display().to_string(), - ) - .as_error()), + None => Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) + .as_error()), } } /// Edits a profile's icon pub async fn edit_icon( - path: &Path, + path: &ProfilePathId, icon_path: Option<&Path>, ) -> crate::Result<()> { let state = State::get().await?; @@ -125,17 +144,17 @@ pub async fn edit_icon( emit_profile( profile.uuid, - profile.path.clone(), + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Edited, ) .await?; Ok(()) } - None => Err(crate::ErrorKind::UnmanagedProfileError( - path.display().to_string(), - ) - .as_error()), + None => { + Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) + .as_error()) + } } } else { edit(path, |profile| { @@ -155,7 +174,7 @@ pub async fn edit_icon( // Generally this would be used for profile_create, to get the optimal JRE key // this can be overwritten by the user a profile-by-profile basis pub async fn get_optimal_jre_key( - path: &Path, + path: &ProfilePathId, ) -> crate::Result> { let state = State::get().await?; @@ -193,10 +212,8 @@ pub async fn get_optimal_jre_key( Ok(version) } else { - Err( - crate::ErrorKind::UnmanagedProfileError(path.display().to_string()) - .as_error(), - ) + Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) + .as_error()) } } @@ -204,7 +221,7 @@ pub async fn get_optimal_jre_key( #[tracing::instrument] pub async fn list( clear_projects: Option, -) -> crate::Result> { +) -> crate::Result> { let state = State::get().await?; let profiles = state.profiles.read().await; Ok(profiles @@ -223,14 +240,12 @@ pub async fn list( /// Installs/Repairs a profile #[tracing::instrument] -pub async fn install(path: &Path) -> crate::Result<()> { +pub async fn install(path: &ProfilePathId) -> crate::Result<()> { if let Some(profile) = get(path, None).await? { crate::launcher::install_minecraft(&profile, None).await?; } else { - return Err(crate::ErrorKind::UnmanagedProfileError( - path.display().to_string(), - ) - .as_error()); + return Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) + .as_error()); } State::sync().await?; Ok(()) @@ -239,12 +254,12 @@ pub async fn install(path: &Path) -> crate::Result<()> { #[tracing::instrument] #[theseus_macros::debug_pin] pub async fn update_all( - profile_path: &Path, -) -> crate::Result> { + profile_path: &ProfilePathId, +) -> crate::Result> { if let Some(profile) = get(profile_path, None).await? { let loading_bar = init_loading( LoadingBarType::ProfileUpdate { - profile_path: profile.path.clone(), + profile_path: profile.get_profile_full_path().await?, profile_name: profile.metadata.name.clone(), }, 100.0, @@ -252,6 +267,7 @@ pub async fn update_all( ) .await?; + let profile_base_path = profile.get_profile_full_path().await?; let keys = profile .projects .into_iter() @@ -272,7 +288,7 @@ pub async fn update_all( use futures::StreamExt; loading_try_for_each_concurrent( - futures::stream::iter(keys).map(Ok::), + futures::stream::iter(keys).map(Ok::), None, Some(&loading_bar), 100.0, @@ -297,7 +313,7 @@ pub async fn update_all( emit_profile( profile.uuid, - profile.path, + profile_base_path, &profile.metadata.name, ProfilePayloadType::Edited, ) @@ -306,20 +322,22 @@ pub async fn update_all( Ok(Arc::try_unwrap(map).unwrap().into_inner()) } else { - Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.display().to_string(), + Err( + crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) + .as_error(), ) - .as_error()) } } +/// Updates a project to the latest version +/// Uses and returns the relative path to the project #[tracing::instrument] #[theseus_macros::debug_pin] pub async fn update_project( - profile_path: &Path, - project_path: &Path, + profile_path: &ProfilePathId, + project_path: &ProjectPathId, skip_send_event: Option, -) -> crate::Result { +) -> crate::Result { if let Some(profile) = get(profile_path, None).await? { if let Some(project) = profile.projects.get(project_path) { if let ProjectMetadata::Modrinth { @@ -331,13 +349,13 @@ pub async fn update_project( .add_project_version(update_version.id.clone()) .await?; - if path != project_path { + if path != project_path.clone() { profile.remove_project(project_path, Some(true)).await?; } let state = State::get().await?; let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(project_path) { + if let Some(profile) = profiles.0.get_mut(profile_path) { let value = profile.projects.remove(project_path); if let Some(mut project) = value { if let ProjectMetadata::Modrinth { @@ -354,7 +372,7 @@ pub async fn update_project( if !skip_send_event.unwrap_or(false) { emit_profile( profile.uuid, - profile.path, + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Edited, ) @@ -371,25 +389,26 @@ pub async fn update_project( ) .as_error()) } else { - Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.display().to_string(), + Err( + crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) + .as_error(), ) - .as_error()) } } /// Add a project from a version +/// Returns the relative path to the project as a ProjectPathId #[tracing::instrument] pub async fn add_project_from_version( - profile_path: &Path, + profile_path: &ProfilePathId, version_id: String, -) -> crate::Result { +) -> crate::Result { if let Some(profile) = get(profile_path, None).await? { let (path, _) = profile.add_project_version(version_id).await?; emit_profile( profile.uuid, - profile.path, + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Edited, ) @@ -398,20 +417,21 @@ pub async fn add_project_from_version( Ok(path) } else { - Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.display().to_string(), + Err( + crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) + .as_error(), ) - .as_error()) } } /// Add a project from an FS path +/// Uses and returns the relative path to the project as a ProjectPathId #[tracing::instrument] pub async fn add_project_from_path( - profile_path: &Path, + profile_path: &ProfilePathId, path: &Path, project_type: Option, -) -> crate::Result { +) -> crate::Result { if let Some(profile) = get(profile_path, None).await? { let file = io::read(path).await?; let file_name = path @@ -430,7 +450,7 @@ pub async fn add_project_from_path( emit_profile( profile.uuid, - profile.path, + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Edited, ) @@ -439,25 +459,27 @@ pub async fn add_project_from_path( Ok(path) } else { - Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.display().to_string(), + Err( + crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) + .as_error(), ) - .as_error()) } } /// Toggle whether a project is disabled or not +/// Project path should be relative to the profile +/// returns the new state, relative to the profile #[tracing::instrument] pub async fn toggle_disable_project( - profile: &Path, - project: &Path, -) -> crate::Result { + profile: &ProfilePathId, + project: &ProjectPathId, +) -> crate::Result { if let Some(profile) = get(profile, None).await? { let res = profile.toggle_disable_project(project).await?; emit_profile( profile.uuid, - profile.path, + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Edited, ) @@ -466,25 +488,24 @@ pub async fn toggle_disable_project( Ok(res) } else { - Err(crate::ErrorKind::UnmanagedProfileError( - profile.display().to_string(), - ) - .as_error()) + Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string()) + .as_error()) } } /// Remove a project from a profile +/// Uses and returns the relative path to the project #[tracing::instrument] pub async fn remove_project( - profile: &Path, - project: &Path, + profile: &ProfilePathId, + project: &ProjectPathId, ) -> crate::Result<()> { if let Some(profile) = get(profile, None).await? { profile.remove_project(project, None).await?; emit_profile( profile.uuid, - profile.path, + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Edited, ) @@ -493,10 +514,8 @@ pub async fn remove_project( Ok(()) } else { - Err(crate::ErrorKind::UnmanagedProfileError( - profile.display().to_string(), - ) - .as_error()) + Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string()) + .as_error()) } } @@ -505,7 +524,7 @@ pub async fn remove_project( #[tracing::instrument(skip_all)] #[theseus_macros::debug_pin] pub async fn export_mrpack( - profile_path: &Path, + profile_path: &ProfilePathId, export_path: PathBuf, included_overrides: Vec, // which folders to include in the overrides version_id: Option, @@ -516,11 +535,24 @@ pub async fn export_mrpack( let profile = get(profile_path, None).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to export a nonexistent or unloaded profile at path {}!", - profile_path.display() + profile_path )) })?; - let profile_base_path = &profile.path; + // remove .DS_Store files from included_overrides + let included_overrides = included_overrides + .into_iter() + .filter(|x| { + if let Some(f) = PathBuf::from(x).file_name() { + if f.to_string_lossy().starts_with(".DS_Store") { + return false; + } + } + true + }) + .collect::>(); + + let profile_base_path = &profile.get_profile_full_path().await?; let mut file = File::create(&export_path) .await @@ -529,7 +561,7 @@ pub async fn export_mrpack( // Create mrpack json configuration file let version_id = version_id.unwrap_or("1.0.0".to_string()); - let packfile = create_mrpack_json(&profile, version_id)?; + let packfile = create_mrpack_json(&profile, version_id).await?; let modrinth_path_list = get_modrinth_pack_list(&packfile); // Build vec of all files in the folder @@ -539,7 +571,7 @@ pub async fn export_mrpack( // Initialize loading bar let loading_bar = init_loading( LoadingBarType::ZipExtract { - profile_path: profile.path.to_path_buf(), + profile_path: profile.get_profile_full_path().await?, profile_name: profile.metadata.name.clone(), }, path_list.len() as f64, @@ -628,25 +660,28 @@ pub async fn export_mrpack( // => [folder1, folder2] #[tracing::instrument] pub async fn get_potential_override_folders( - profile_path: PathBuf, + profile_path: ProfilePathId, ) -> crate::Result> { // First, get a dummy mrpack json for the files within let profile: Profile = get(&profile_path, None).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to export a nonexistent or unloaded profile at path {}!", - profile_path.display() + profile_path )) })?; - let mrpack = create_mrpack_json(&profile, "0".to_string())?; + // dummy mrpack to get pack list + let mrpack = create_mrpack_json(&profile, "0".to_string()).await?; let mrpack_files = get_modrinth_pack_list(&mrpack); let mut path_list: Vec = Vec::new(); - let mut read_dir = io::read_dir(&profile_path).await?; + + let profile_base_dir = profile.get_profile_full_path().await?; + let mut read_dir = io::read_dir(&profile_base_dir).await?; while let Some(entry) = read_dir .next_entry() .await - .map_err(|e| IOError::with_path(e, &profile_path))? + .map_err(|e| IOError::with_path(e, &profile_base_dir))? { let path: PathBuf = entry.path(); if path.is_dir() { @@ -655,17 +690,17 @@ pub async fn get_potential_override_folders( while let Some(entry) = read_dir .next_entry() .await - .map_err(|e| IOError::with_path(e, &profile_path))? + .map_err(|e| IOError::with_path(e, &profile_base_dir))? { let path: PathBuf = entry.path(); - let name = path.strip_prefix(&profile_path)?.to_path_buf(); + let name = path.strip_prefix(&profile_base_dir)?.to_path_buf(); if !mrpack_files.contains(&name.to_string_lossy().to_string()) { path_list.push(name); } } } else { // One layer of files/folders if its a file - let name = path.strip_prefix(&profile_path)?.to_path_buf(); + let name = path.strip_prefix(&profile_base_dir)?.to_path_buf(); if !mrpack_files.contains(&name.to_string_lossy().to_string()) { path_list.push(name); } @@ -677,7 +712,9 @@ pub async fn get_potential_override_folders( /// Run Minecraft using a profile and the default credentials, logged in credentials, /// failing with an error if no credentials are available #[tracing::instrument] -pub async fn run(path: &Path) -> crate::Result>> { +pub async fn run( + path: &ProfilePathId, +) -> crate::Result>> { let state = State::get().await?; // Get default account and refresh credentials (preferred way to log in) @@ -702,7 +739,7 @@ pub async fn run(path: &Path) -> crate::Result>> { #[tracing::instrument(skip(credentials))] #[theseus_macros::debug_pin] pub async fn run_credentials( - path: &Path, + path: &ProfilePathId, credentials: &auth::Credentials, ) -> crate::Result>> { let state = State::get().await?; @@ -710,7 +747,7 @@ pub async fn run_credentials( let profile = get(path, None).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to run a nonexistent or unloaded profile at path {}!", - path.display() + path )) })?; @@ -720,11 +757,12 @@ pub async fn run_credentials( // TODO: hook parameters let mut cmd = hook.split(' '); if let Some(command) = cmd.next() { + let full_path = path.get_full_path().await?; let result = Command::new(command) .args(&cmd.collect::>()) - .current_dir(path) + .current_dir(&full_path) .spawn() - .map_err(|e| IOError::with_path(e, path))? + .map_err(|e| IOError::with_path(e, &full_path))? .wait() .await .map_err(IOError::from)?; @@ -767,7 +805,9 @@ pub async fn run_credentials( let mut cmd = hook.split(' '); if let Some(command) = cmd.next() { let mut command = Command::new(command); - command.args(&cmd.collect::>()).current_dir(path); + command + .args(&cmd.collect::>()) + .current_dir(path.get_full_path().await?); Some(command) } else { None @@ -806,7 +846,7 @@ fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec { /// Creates a json configuration for a .mrpack zipped file // Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) #[tracing::instrument(skip_all)] -pub fn create_mrpack_json( +pub async fn create_mrpack_json( profile: &Profile, version_id: String, ) -> crate::Result { @@ -845,17 +885,15 @@ pub fn create_mrpack_json( .map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string())) .collect::>(); - let base_path = &profile.path; + let profile_base_path = profile.get_profile_full_path().await?; let files: Result, crate::ErrorKind> = profile .projects .iter() .filter_map(|(mod_path, project)| { - let path = match mod_path.strip_prefix(base_path) { - Ok(path) => path.to_string_lossy().to_string(), - Err(e) => { - return Some(Err(e.into())); - } - }; + let path: String = profile_base_path + .join(mod_path.0.clone()) + .to_string_lossy() + .to_string(); // Only Modrinth projects have a modrinth metadata field for the modrinth.json Some(Ok(match project.metadata { diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile_create.rs index e03418ba..9a6747d8 100644 --- a/theseus/src/api/profile_create.rs +++ b/theseus/src/api/profile_create.rs @@ -1,5 +1,5 @@ //! Theseus profile management interface -use crate::state::LinkedData; +use crate::state::{LinkedData, ProfilePathId}; use crate::util::io::{self, canonicalize}; use crate::{ event::{emit::emit_profile, ProfilePayloadType}, @@ -10,20 +10,18 @@ pub use crate::{ State, }; use daedalus::modded::LoaderVersion; -use futures::prelude::*; - use std::path::PathBuf; -use tokio_stream::wrappers::ReadDirStream; + use tracing::{info, trace}; use uuid::Uuid; -// Creates a profile at the given filepath and adds it to the in-memory state -// Returns filepath at which it can be accessed in the State +// Creates a profile of a given name and adds it to the in-memory state +// Returns relative filepath as ProfilePathId which can be used to access it in the State #[tracing::instrument] #[theseus_macros::debug_pin] #[allow(clippy::too_many_arguments)] pub async fn profile_create( - name: String, // the name of the profile, and relative path + mut name: String, // the name of the profile, and relative path game_version: String, // the game version of the profile modloader: ModLoader, // the modloader to use loader_version: Option, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest @@ -31,32 +29,34 @@ pub async fn profile_create( icon_url: Option, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES) linked_data: Option, // the linked project ID (mainly for modpacks)- used for updating skip_install_profile: Option, -) -> crate::Result { +) -> crate::Result { trace!("Creating new profile. {}", name); let state = State::get().await?; let uuid = Uuid::new_v4(); - let path = state.directories.profiles_dir().join(uuid.to_string()); + + let mut path = state.directories.profiles_dir().await.join(&name); if path.exists() { - if !path.is_dir() { - return Err(ProfileCreationError::NotFolder.into()); - } - if path.join("profile.json").exists() { - return Err(ProfileCreationError::ProfileExistsError( - path.join("profile.json"), - ) - .into()); + let mut new_name; + let mut new_path; + let mut which = 1; + loop { + new_name = format!("{name} ({which})"); + new_path = state.directories.profiles_dir().await.join(&new_name); + if !new_path.exists() { + break; + } + which += 1; } - if ReadDirStream::new(io::read_dir(&path).await?) - .next() - .await - .is_some() - { - return Err(ProfileCreationError::NotEmptyFolder.into()); - } - } else { - io::create_dir_all(&path).await?; + tracing::debug!( + "Folder collision: {}, renaming to: {}", + path.display(), + new_path.display() + ); + path = new_path; + name = new_name; } + io::create_dir_all(&path).await?; info!( "Creating profile at path {}", @@ -73,13 +73,11 @@ pub async fn profile_create( None }; - // Fully canonicalize now that its created for storing purposes - let path = canonicalize(&path)?; - let mut profile = - Profile::new(uuid, name, game_version, path.clone()).await?; + let mut profile = Profile::new(uuid, name, game_version).await?; let result = async { if let Some(ref icon) = icon { - let bytes = io::read(icon).await?; + let bytes = + io::read(state.directories.caches_dir().join(icon)).await?; profile .set_icon( &state.directories.caches_dir(), @@ -100,7 +98,7 @@ pub async fn profile_create( emit_profile( uuid, - path.clone(), + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Created, ) @@ -116,14 +114,14 @@ pub async fn profile_create( } State::sync().await?; - Ok(path) + Ok(profile.profile_id()) } .await; match result { Ok(profile) => Ok(profile), Err(err) => { - let _ = crate::api::profile::remove(&profile.path).await; + let _ = crate::api::profile::remove(&profile.profile_id()).await; Err(err) } diff --git a/theseus/src/api/settings.rs b/theseus/src/api/settings.rs index e5486d57..aa33dcc0 100644 --- a/theseus/src/api/settings.rs +++ b/theseus/src/api/settings.rs @@ -1,5 +1,16 @@ //! Theseus profile management interface +use std::path::PathBuf; + +use io::IOError; +use tokio::sync::RwLock; + +use crate::{ + event::emit::{emit_loading, init_loading}, + prelude::DirectoryInfo, + state::{self, Profiles}, + util::io, +}; pub use crate::{ state::{ Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, @@ -19,6 +30,16 @@ pub async fn get() -> crate::Result { #[tracing::instrument] pub async fn set(settings: Settings) -> crate::Result<()> { let state = State::get().await?; + + if settings.loaded_config_dir + != state.settings.read().await.loaded_config_dir + { + return Err(crate::ErrorKind::OtherError( + "Cannot change config directory as setting".to_string(), + ) + .as_error()); + } + let (reset_io, reset_fetch) = async { let read = state.settings.read().await; ( @@ -42,3 +63,119 @@ pub async fn set(settings: Settings) -> crate::Result<()> { State::sync().await?; Ok(()) } + +/// Sets the new config dir, the location of all Theseus data except for the settings.json and caches +/// Takes control of the entire state and blocks until completion +pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> { + if !new_config_dir.is_dir() { + return Err(crate::ErrorKind::FSError(format!( + "New config dir is not a folder: {}", + new_config_dir.display() + )) + .as_error()); + } + + let loading_bar = init_loading( + crate::LoadingBarType::ConfigChange { + new_path: new_config_dir.clone(), + }, + 100.0, + "Changing configuration directory", + ) + .await?; + + tracing::trace!("Changing config dir, taking control of the state"); + // Take control of the state + let mut state_write = State::get_write().await?; + let old_config_dir = + state_write.directories.config_dir.read().await.clone(); + + tracing::trace!("Setting configuration setting"); + // Set load config dir setting + let settings = { + let mut settings = state_write.settings.write().await; + settings.loaded_config_dir = Some(new_config_dir.clone()); + + // Some java paths are hardcoded to within our config dir, so we need to update them + tracing::trace!("Updating java keys"); + for key in settings.java_globals.keys() { + if let Some(java) = settings.java_globals.get_mut(&key) { + // If the path is within the old config dir path, update it to the new config dir + if let Ok(relative_path) = PathBuf::from(java.path.clone()) + .strip_prefix(&old_config_dir) + { + java.path = new_config_dir + .join(relative_path) + .to_string_lossy() + .to_string(); + } + } + } + tracing::trace!("Syncing settings"); + + settings + .sync(&state_write.directories.settings_file()) + .await?; + settings.clone() + }; + + tracing::trace!("Reinitializing directory"); + // Set new state information + state_write.directories = DirectoryInfo::init(&settings)?; + let total_entries = std::fs::read_dir(&old_config_dir) + .map_err(|e| IOError::with_path(e, &old_config_dir))? + .count() as f64; + + // Move all files over from state_write.directories.config_dir to new_config_dir + tracing::trace!("Renaming folder structure"); + let mut i = 0.0; + let mut entries = io::read_dir(&old_config_dir).await?; + while let Some(entry) = entries + .next_entry() + .await + .map_err(|e| IOError::with_path(e, &old_config_dir))? + { + let entry_path = entry.path(); + if let Some(file_name) = entry_path.file_name() { + // Ignore settings.json + if file_name == state::SETTINGS_FILE_NAME { + continue; + } + // Ignore caches folder + if file_name == state::CACHES_FOLDER_NAME { + continue; + } + // Ignore modrinth_logs folder + if file_name == state::LAUNCHER_LOGS_FOLDER_NAME { + continue; + } + + let new_path = new_config_dir.join(file_name); + io::rename(entry_path, new_path).await?; + + i += 1.0; + emit_loading(&loading_bar, 90.0 * (i / total_entries), None) + .await?; + } + } + + // Reset file watcher + tracing::trace!("Reset file watcher"); + let mut file_watcher = state::init_watcher().await?; + + // Reset profiles (for filepaths, file watcher, etc) + state_write.profiles = RwLock::new( + Profiles::init(&state_write.directories, &mut file_watcher).await?, + ); + state_write.file_watcher = RwLock::new(file_watcher); + + emit_loading(&loading_bar, 10.0, None).await?; + + // TODO: need to be able to safely error out of this function, reverting the changes + tracing::info!( + "Successfully switched config folder to: {}", + new_config_dir.display() + ); + + Ok(()) +} diff --git a/theseus/src/error.rs b/theseus/src/error.rs index da1928dc..fb2485be 100644 --- a/theseus/src/error.rs +++ b/theseus/src/error.rs @@ -61,7 +61,7 @@ pub enum ErrorKind { #[error("Error acquiring semaphore: {0}")] AcquireError(#[from] tokio::sync::AcquireError), - #[error("Profile {0} is not managed by Theseus!")] + #[error("Profile {0} is not managed by the app!")] UnmanagedProfileError(String), #[error("Could not create profile: {0}")] diff --git a/theseus/src/event/mod.rs b/theseus/src/event/mod.rs index 27c3a0fd..2d5f5370 100644 --- a/theseus/src/event/mod.rs +++ b/theseus/src/event/mod.rs @@ -181,6 +181,9 @@ pub enum LoadingBarType { profile_path: PathBuf, profile_name: String, }, + ConfigChange { + new_path: PathBuf, + }, } #[derive(Serialize, Clone)] diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index 5528c487..78481de2 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -68,6 +68,7 @@ pub async fn download_version_info( let path = st .directories .version_dir(&version_id) + .await .join(format!("{version_id}.json")); let res = if path.exists() && !force.unwrap_or(false) { @@ -118,6 +119,7 @@ pub async fn download_client( let path = st .directories .version_dir(version) + .await .join(format!("{version}.jar")); if !path.exists() { @@ -149,6 +151,7 @@ pub async fn download_assets_index( let path = st .directories .assets_index_dir() + .await .join(format!("{}.json", &version.asset_index.id)); let res = if path.exists() { @@ -192,7 +195,7 @@ pub async fn download_assets( None, |(name, asset)| async move { let hash = &asset.hash; - let resource_path = st.directories.object_dir(hash); + let resource_path = st.directories.object_dir(hash).await; let url = format!( "https://resources.download.minecraft.net/{sub_hash}/{hash}", sub_hash = &hash[..2] @@ -215,7 +218,7 @@ pub async fn download_assets( let resource = fetch_cell .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .await?; - let resource_path = st.directories.legacy_assets_dir().join( + let resource_path = st.directories.legacy_assets_dir().await.join( name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) ); write(&resource_path, resource, &st.io_semaphore).await?; @@ -245,8 +248,8 @@ pub async fn download_libraries( tracing::debug!("Loading libraries"); tokio::try_join! { - io::create_dir_all(st.directories.libraries_dir()), - io::create_dir_all(st.directories.version_natives_dir(version)) + io::create_dir_all(st.directories.libraries_dir().await), + io::create_dir_all(st.directories.version_natives_dir(version).await) }?; let num_files = libraries.len(); loading_try_for_each_concurrent( @@ -262,7 +265,7 @@ pub async fn download_libraries( tokio::try_join! { async { let artifact_path = d::get_path_from_artifact(&library.name)?; - let path = st.directories.libraries_dir().join(&artifact_path); + let path = st.directories.libraries_dir().await.join(&artifact_path); match library.downloads { _ if path.exists() => Ok(()), @@ -314,7 +317,7 @@ pub async fn download_libraries( let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?; let reader = std::io::Cursor::new(&data); if let Ok(mut archive) = zip::ZipArchive::new(reader) { - match archive.extract(&st.directories.version_natives_dir(version)) { + match archive.extract(st.directories.version_natives_dir(version).await) { Ok(_) => tracing::info!("Fetched native {}", &library.name), Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err) } diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index b5e86637..89ec1a70 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -108,14 +108,14 @@ pub async fn install_minecraft( LoadingBarType::MinecraftDownload { // If we are downloading minecraft for a profile, provide its name and uuid profile_name: profile.metadata.name.clone(), - profile_path: profile.path.clone(), + profile_path: profile.get_profile_full_path().await?, }, 100.0, "Downloading Minecraft", ) .await?; - crate::api::profile::edit(&profile.path, |prof| { + crate::api::profile::edit(&profile.profile_id(), |prof| { prof.install_stage = ProfileInstallStage::Installing; async { Ok(()) } @@ -124,7 +124,8 @@ pub async fn install_minecraft( State::sync().await?; let state = State::get().await?; - let instance_path = &io::canonicalize(&profile.path)?; + let instance_path = + &io::canonicalize(&profile.get_profile_full_path().await?)?; let metadata = state.metadata.read().await; let version = metadata @@ -176,8 +177,11 @@ pub async fn install_minecraft( let client_path = state .directories .version_dir(&version_jar) + .await .join(format!("{version_jar}.jar")); + let libraries_dir = state.directories.libraries_dir().await; + if let Some(ref mut data) = version_info.data { processor_rules! { data; @@ -194,7 +198,7 @@ pub async fn install_minecraft( client => instance_path.to_string_lossy(), server => ""; "LIBRARY_DIR": - client => state.directories.libraries_dir().to_string_lossy(), + client => libraries_dir.to_string_lossy(), server => ""; } @@ -217,13 +221,13 @@ pub async fn install_minecraft( let child = Command::new(&java_version.path) .arg("-cp") .arg(args::get_class_paths_jar( - &state.directories.libraries_dir(), + &libraries_dir, &cp, &java_version.architecture, )?) .arg( args::get_processor_main_class(args::get_lib_path( - &state.directories.libraries_dir(), + &libraries_dir, &processor.jar, false, )?) @@ -236,7 +240,7 @@ pub async fn install_minecraft( })?, ) .args(args::get_processor_arguments( - &state.directories.libraries_dir(), + &libraries_dir, &processor.args, data, )?) @@ -269,7 +273,7 @@ pub async fn install_minecraft( } } - crate::api::profile::edit(&profile.path, |prof| { + crate::api::profile::edit(&profile.profile_id(), |prof| { prof.install_stage = ProfileInstallStage::Installed; async { Ok(()) } @@ -309,7 +313,9 @@ pub async fn launch_minecraft( let state = State::get().await?; let metadata = state.metadata.read().await; - let instance_path = &io::canonicalize(&profile.path)?; + + let instance_path = profile.get_profile_full_path().await?; + let instance_path = &io::canonicalize(instance_path)?; let version = metadata .minecraft @@ -359,6 +365,7 @@ pub async fn launch_minecraft( let client_path = state .directories .version_dir(&version_jar) + .await .join(format!("{version_jar}.jar")); let args = version_info.arguments.clone().unwrap_or_default(); @@ -374,11 +381,11 @@ pub async fn launch_minecraft( // Check if profile has a running profile, and reject running the command if it does // Done late so a quick double call doesn't launch two instances let existing_processes = - process::get_uuids_by_profile_path(instance_path).await?; + process::get_uuids_by_profile_path(profile.profile_id()).await?; if let Some(uuid) = existing_processes.first() { return Err(crate::ErrorKind::LauncherError(format!( "Profile {} is already running at UUID: {uuid}", - instance_path.display() + profile.profile_id() )) .as_error()); } @@ -388,10 +395,10 @@ pub async fn launch_minecraft( args::get_jvm_arguments( args.get(&d::minecraft::ArgumentType::Jvm) .map(|x| x.as_slice()), - &state.directories.version_natives_dir(&version_jar), - &state.directories.libraries_dir(), + &state.directories.version_natives_dir(&version_jar).await, + &state.directories.libraries_dir().await, &args::get_class_paths( - &state.directories.libraries_dir(), + &state.directories.libraries_dir().await, version_info.libraries.as_slice(), &client_path, &java_version.architecture, @@ -414,7 +421,7 @@ pub async fn launch_minecraft( &version.id, &version_info.asset_index.id, instance_path, - &state.directories.assets_dir(), + &state.directories.assets_dir().await, &version.type_, *resolution, &java_version.architecture, @@ -439,14 +446,15 @@ pub async fn launch_minecraft( let logs_dir = { let st = State::get().await?; st.directories - .profile_logs_dir(profile.uuid) + .profile_logs_dir(&profile.profile_id()) + .await? .join(&datetime_string) }; io::create_dir_all(&logs_dir).await?; let stdout_log_path = logs_dir.join("stdout.log"); - crate::api::profile::edit(&profile.path, |prof| { + crate::api::profile::edit(&profile.profile_id(), |prof| { prof.metadata.last_played = Some(Utc::now()); async { Ok(()) } @@ -499,7 +507,7 @@ pub async fn launch_minecraft( state_children .insert_process( Uuid::new_v4(), - instance_path.to_path_buf(), + profile.profile_id(), stdout_log_path, command, post_exit_hook, diff --git a/theseus/src/logger.rs b/theseus/src/logger.rs index 05e64f58..b715c0cc 100644 --- a/theseus/src/logger.rs +++ b/theseus/src/logger.rs @@ -44,10 +44,10 @@ pub fn start_logger() -> Option { use tracing_subscriber::prelude::*; // Initialize and get logs directory path - let path = if let Some(dir) = DirectoryInfo::init().ok() { - dir.launcher_logs_dir() + let logs_dir = if let Some(d) = DirectoryInfo::launcher_logs_dir() { + d } else { - eprintln!("Could not create logger."); + eprintln!("Could not start logger"); return None; }; @@ -55,7 +55,7 @@ pub fn start_logger() -> Option { .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info")); let file_appender = - RollingFileAppender::new(Rotation::DAILY, path, "theseus.log"); + RollingFileAppender::new(Rotation::DAILY, logs_dir, "theseus.log"); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); let subscriber = tracing_subscriber::registry() diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index 2ed0c511..c68fe299 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -1,4 +1,4 @@ -use super::Profile; +use super::{Profile, ProfilePathId}; use std::path::{Path, PathBuf}; use std::process::ExitStatus; use std::{collections::HashMap, sync::Arc}; @@ -25,7 +25,7 @@ pub struct Children(HashMap>>); #[derive(Debug)] pub struct MinecraftChild { pub uuid: Uuid, - pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID + pub profile_relative_path: ProfilePathId, pub manager: Option>>, // None when future has completed and been handled pub current_child: Arc>, pub output: SharedOutput, @@ -53,7 +53,7 @@ impl Children { pub async fn insert_process( &mut self, uuid: Uuid, - profile_path: PathBuf, + profile_relative_path: ProfilePathId, log_path: PathBuf, mut mc_command: Command, post_command: Option, // Command to run after minecraft. @@ -107,7 +107,7 @@ impl Children { // Create MinecraftChild let mchild = MinecraftChild { uuid, - profile_path, + profile_relative_path, current_child, output: shared_output, manager, @@ -266,7 +266,7 @@ impl Children { // Gets all PID keys of running children with a given profile path pub async fn running_keys_with_profile( &self, - profile_path: &Path, + profile_path: ProfilePathId, ) -> crate::Result> { let running_keys = self.running_keys().await?; let mut keys = Vec::new(); @@ -274,7 +274,7 @@ impl Children { if let Some(child) = self.get(&key) { let child = child.clone(); let child = child.read().await; - if child.profile_path == profile_path { + if child.profile_relative_path == profile_path { keys.push(key); } } @@ -283,7 +283,9 @@ impl Children { } // Gets all profiles of running children - pub async fn running_profile_paths(&self) -> crate::Result> { + pub async fn running_profile_paths( + &self, + ) -> crate::Result> { let mut profiles = Vec::new(); for key in self.keys() { if let Some(child) = self.get(&key) { @@ -297,7 +299,7 @@ impl Children { .map_err(IOError::from)? .is_none() { - profiles.push(child.profile_path.clone()); + profiles.push(child.profile_relative_path.clone()); } } } @@ -321,7 +323,7 @@ impl Children { .is_none() { if let Some(prof) = crate::api::profile::get( - &child.profile_path.clone(), + &child.profile_relative_path.clone(), None, ) .await? diff --git a/theseus/src/state/dirs.rs b/theseus/src/state/dirs.rs index f564e7d7..9a626606 100644 --- a/theseus/src/state/dirs.rs +++ b/theseus/src/state/dirs.rs @@ -2,16 +2,42 @@ use std::fs; use std::path::PathBuf; +use tokio::sync::RwLock; + +use super::{ProfilePathId, Settings}; + +pub const SETTINGS_FILE_NAME: &str = "settings.json"; +pub const CACHES_FOLDER_NAME: &str = "caches"; +pub const LAUNCHER_LOGS_FOLDER_NAME: &str = "launcher_logs"; + #[derive(Debug)] pub struct DirectoryInfo { - pub config_dir: PathBuf, + pub settings_dir: PathBuf, // Base settings directory- settings.json and icon cache. + pub config_dir: RwLock, // Base config directory- instances, minecraft downloads, etc. Changeable as a setting. pub working_dir: PathBuf, } impl DirectoryInfo { + // Get the settings directory + // init() is not needed for this function + pub fn get_initial_settings_dir() -> Option { + Self::env_path("THESEUS_CONFIG_DIR") + .or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus"))) + } + + #[inline] + pub fn get_initial_settings_file() -> crate::Result { + let settings_dir = Self::get_initial_settings_dir().ok_or( + crate::ErrorKind::FSError( + "Could not find valid config dir".to_string(), + ), + )?; + Ok(settings_dir.join("settings.json")) + } + /// Get all paths needed for Theseus to operate properly #[tracing::instrument] - pub fn init() -> crate::Result { + pub fn init(settings: &Settings) -> crate::Result { // Working directory let working_dir = std::env::current_dir().map_err(|err| { crate::ErrorKind::FSError(format!( @@ -19,143 +45,153 @@ impl DirectoryInfo { )) })?; - // Config directory - let config_dir = Self::env_path("THESEUS_CONFIG_DIR") - .or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus"))) - .ok_or(crate::ErrorKind::FSError( - "Could not find valid config dir".to_string(), - ))?; + let settings_dir = Self::get_initial_settings_dir().ok_or( + crate::ErrorKind::FSError( + "Could not find valid settings dir".to_string(), + ), + )?; - fs::create_dir_all(&config_dir).map_err(|err| { + fs::create_dir_all(&settings_dir).map_err(|err| { crate::ErrorKind::FSError(format!( "Error creating Theseus config directory: {err}" )) })?; + // config directory (for instances, etc.) + // by default this is the same as the settings directory + let config_dir = settings.loaded_config_dir.clone().ok_or( + crate::ErrorKind::FSError( + "Could not find valid config dir".to_string(), + ), + )?; + Ok(Self { - config_dir, + settings_dir, + config_dir: RwLock::new(config_dir), working_dir, }) } /// Get the Minecraft instance metadata directory #[inline] - pub fn metadata_dir(&self) -> PathBuf { - self.config_dir.join("meta") + pub async fn metadata_dir(&self) -> PathBuf { + self.config_dir.read().await.join("meta") } /// Get the Minecraft java versions metadata directory #[inline] - pub fn java_versions_dir(&self) -> PathBuf { - self.metadata_dir().join("java_versions") + pub async fn java_versions_dir(&self) -> PathBuf { + self.metadata_dir().await.join("java_versions") } /// Get the Minecraft versions metadata directory #[inline] - pub fn versions_dir(&self) -> PathBuf { - self.metadata_dir().join("versions") + pub async fn versions_dir(&self) -> PathBuf { + self.metadata_dir().await.join("versions") } /// Get the metadata directory for a given version #[inline] - pub fn version_dir(&self, version: &str) -> PathBuf { - self.versions_dir().join(version) + pub async fn version_dir(&self, version: &str) -> PathBuf { + self.versions_dir().await.join(version) } /// Get the Minecraft libraries metadata directory #[inline] - pub fn libraries_dir(&self) -> PathBuf { - self.metadata_dir().join("libraries") + pub async fn libraries_dir(&self) -> PathBuf { + self.metadata_dir().await.join("libraries") } /// Get the Minecraft assets metadata directory #[inline] - pub fn assets_dir(&self) -> PathBuf { - self.metadata_dir().join("assets") + pub async fn assets_dir(&self) -> PathBuf { + self.metadata_dir().await.join("assets") } /// Get the assets index directory #[inline] - pub fn assets_index_dir(&self) -> PathBuf { - self.assets_dir().join("indexes") + pub async fn assets_index_dir(&self) -> PathBuf { + self.assets_dir().await.join("indexes") } /// Get the assets objects directory #[inline] - pub fn objects_dir(&self) -> PathBuf { - self.assets_dir().join("objects") + pub async fn objects_dir(&self) -> PathBuf { + self.assets_dir().await.join("objects") } /// Get the directory for a specific object #[inline] - pub fn object_dir(&self, hash: &str) -> PathBuf { - self.objects_dir().join(&hash[..2]).join(hash) + pub async fn object_dir(&self, hash: &str) -> PathBuf { + self.objects_dir().await.join(&hash[..2]).join(hash) } /// Get the Minecraft legacy assets metadata directory #[inline] - pub fn legacy_assets_dir(&self) -> PathBuf { - self.metadata_dir().join("resources") + pub async fn legacy_assets_dir(&self) -> PathBuf { + self.metadata_dir().await.join("resources") } /// Get the Minecraft legacy assets metadata directory #[inline] - pub fn natives_dir(&self) -> PathBuf { - self.metadata_dir().join("natives") + pub async fn natives_dir(&self) -> PathBuf { + self.metadata_dir().await.join("natives") } /// Get the natives directory for a version of Minecraft #[inline] - pub fn version_natives_dir(&self, version: &str) -> PathBuf { - self.natives_dir().join(version) + pub async fn version_natives_dir(&self, version: &str) -> PathBuf { + self.natives_dir().await.join(version) } /// Get the directory containing instance icons #[inline] - pub fn icon_dir(&self) -> PathBuf { - self.config_dir.join("icons") + pub async fn icon_dir(&self) -> PathBuf { + self.config_dir.read().await.join("icons") } /// Get the profiles directory for created profiles #[inline] - pub fn profiles_dir(&self) -> PathBuf { - self.config_dir.join("profiles") + pub async fn profiles_dir(&self) -> PathBuf { + self.config_dir.read().await.join("profiles") } /// Gets the logs dir for a given profile #[inline] - pub fn profile_logs_dir(&self, profile: uuid::Uuid) -> PathBuf { - self.profiles_dir() - .join(profile.to_string()) - .join("modrinth_logs") + pub async fn profile_logs_dir( + &self, + profile_id: &ProfilePathId, + ) -> crate::Result { + Ok(profile_id.get_full_path().await?.join("modrinth_logs")) } #[inline] - pub fn launcher_logs_dir(&self) -> PathBuf { - self.config_dir.join("launcher_logs") + pub fn launcher_logs_dir() -> Option { + Self::get_initial_settings_dir() + .map(|d| d.join(LAUNCHER_LOGS_FOLDER_NAME)) } /// Get the file containing the global database #[inline] - pub fn database_file(&self) -> PathBuf { - self.config_dir.join("data.bin") + pub async fn database_file(&self) -> PathBuf { + self.config_dir.read().await.join("data.bin") } /// Get the settings file for Theseus #[inline] pub fn settings_file(&self) -> PathBuf { - self.config_dir.join("settings.json") + self.settings_dir.join(SETTINGS_FILE_NAME) } /// Get the cache directory for Theseus #[inline] pub fn caches_dir(&self) -> PathBuf { - self.config_dir.join("caches") + self.settings_dir.join(CACHES_FOLDER_NAME) } #[inline] - pub fn caches_meta_dir(&self) -> PathBuf { - self.config_dir.join("caches").join("metadata") + pub async fn caches_meta_dir(&self) -> PathBuf { + self.caches_dir().join("metadata") } /// Get path from environment variable diff --git a/theseus/src/state/java_globals.rs b/theseus/src/state/java_globals.rs index 9ef33bc1..10b8071e 100644 --- a/theseus/src/state/java_globals.rs +++ b/theseus/src/state/java_globals.rs @@ -35,6 +35,10 @@ impl JavaGlobals { self.0.len() } + pub fn keys(&self) -> Vec { + self.0.keys().cloned().collect() + } + // Validates that every path here is a valid Java version and that the version matches the version stored here // If false, when checked, the user should be prompted to reselect the Java version pub async fn is_all_valid(&self) -> bool { diff --git a/theseus/src/state/metadata.rs b/theseus/src/state/metadata.rs index 38830088..a750a15a 100644 --- a/theseus/src/state/metadata.rs +++ b/theseus/src/state/metadata.rs @@ -61,7 +61,7 @@ impl Metadata { io_semaphore: &IoSemaphore, ) -> crate::Result { let mut metadata = None; - let metadata_path = dirs.caches_meta_dir().join("metadata.json"); + let metadata_path = dirs.caches_meta_dir().await.join("metadata.json"); if let Ok(metadata_json) = read_json::(&metadata_path, io_semaphore).await @@ -106,8 +106,11 @@ impl Metadata { let metadata_fetch = Metadata::fetch().await?; let state = State::get().await?; - let metadata_path = - state.directories.caches_meta_dir().join("metadata.json"); + let metadata_path = state + .directories + .caches_meta_dir() + .await + .join("metadata.json"); write( &metadata_path, diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 76b4623c..5334194a 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -49,7 +49,8 @@ mod safe_processes; pub use self::safe_processes::*; // Global state -static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); +// RwLock on state only has concurrent reads, except for config dir change which takes control of the State +static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); pub struct State { /// Information on the location of files used in the launcher pub directories: DirectoryInfo, @@ -86,83 +87,97 @@ pub struct State { impl State { /// Get the current launcher state, initializing it if needed + pub async fn get( + ) -> crate::Result>> { + Ok(Arc::new( + LAUNCHER_STATE + .get_or_try_init(Self::initialize_state) + .await? + .read() + .await, + )) + } + + /// Get the current launcher state, initializing it if needed + /// Takes writing control of the state, blocking all other uses of it + /// Only used for state change such as changing the config directory + pub async fn get_write( + ) -> crate::Result> { + Ok(LAUNCHER_STATE + .get_or_try_init(Self::initialize_state) + .await? + .write() + .await) + } + #[tracing::instrument] #[theseus_macros::debug_pin] - pub async fn get() -> crate::Result> { - LAUNCHER_STATE - .get_or_try_init(|| { - async { - let loading_bar = init_loading_unsafe( - LoadingBarType::StateInit, - 100.0, - "Initializing launcher", - ) - .await?; + async fn initialize_state() -> crate::Result> { + let loading_bar = init_loading_unsafe( + LoadingBarType::StateInit, + 100.0, + "Initializing launcher", + ) + .await?; - let mut file_watcher = init_watcher().await?; + // Settings + let settings = + Settings::init(&DirectoryInfo::get_initial_settings_file()?) + .await?; - let directories = DirectoryInfo::init()?; - emit_loading(&loading_bar, 10.0, None).await?; + let directories = DirectoryInfo::init(&settings)?; - // Settings - let settings = - Settings::init(&directories.settings_file()).await?; - let fetch_semaphore = FetchSemaphore(RwLock::new( - Semaphore::new(settings.max_concurrent_downloads), - )); - let io_semaphore = IoSemaphore(RwLock::new( - Semaphore::new(settings.max_concurrent_writes), - )); - emit_loading(&loading_bar, 10.0, None).await?; + emit_loading(&loading_bar, 10.0, None).await?; - let metadata_fut = - Metadata::init(&directories, &io_semaphore); - let profiles_fut = - Profiles::init(&directories, &mut file_watcher); - let tags_fut = Tags::init( - &directories, - &io_semaphore, - &fetch_semaphore, - ); - let users_fut = Users::init(&directories, &io_semaphore); - // Launcher data - let (metadata, profiles, tags, users) = loading_join! { - Some(&loading_bar), 70.0, Some("Loading metadata"); - metadata_fut, - profiles_fut, - tags_fut, - users_fut, - }?; + let mut file_watcher = init_watcher().await?; - let children = Children::new(); - let auth_flow = AuthTask::new(); - let safety_processes = SafeProcesses::new(); - emit_loading(&loading_bar, 10.0, None).await?; + let fetch_semaphore = FetchSemaphore(RwLock::new(Semaphore::new( + settings.max_concurrent_downloads, + ))); + let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new( + settings.max_concurrent_writes, + ))); + emit_loading(&loading_bar, 10.0, None).await?; - Ok(Arc::new(Self { - directories, - fetch_semaphore, - fetch_semaphore_max: RwLock::new( - settings.max_concurrent_downloads as u32, - ), - io_semaphore, - io_semaphore_max: RwLock::new( - settings.max_concurrent_writes as u32, - ), - metadata: RwLock::new(metadata), - settings: RwLock::new(settings), - profiles: RwLock::new(profiles), - users: RwLock::new(users), - children: RwLock::new(children), - auth_flow: RwLock::new(auth_flow), - tags: RwLock::new(tags), - safety_processes: RwLock::new(safety_processes), - file_watcher: RwLock::new(file_watcher), - })) - } - }) - .await - .map(Arc::clone) + let metadata_fut = Metadata::init(&directories, &io_semaphore); + let profiles_fut = Profiles::init(&directories, &mut file_watcher); + let tags_fut = + Tags::init(&directories, &io_semaphore, &fetch_semaphore); + let users_fut = Users::init(&directories, &io_semaphore); + // Launcher data + let (metadata, profiles, tags, users) = loading_join! { + Some(&loading_bar), 70.0, Some("Loading metadata"); + metadata_fut, + profiles_fut, + tags_fut, + users_fut, + }?; + + let children = Children::new(); + let auth_flow = AuthTask::new(); + let safety_processes = SafeProcesses::new(); + emit_loading(&loading_bar, 10.0, None).await?; + + Ok::, crate::Error>(RwLock::new(Self { + directories, + fetch_semaphore, + fetch_semaphore_max: RwLock::new( + settings.max_concurrent_downloads as u32, + ), + io_semaphore, + io_semaphore_max: RwLock::new( + settings.max_concurrent_writes as u32, + ), + metadata: RwLock::new(metadata), + settings: RwLock::new(settings), + profiles: RwLock::new(profiles), + users: RwLock::new(users), + children: RwLock::new(children), + auth_flow: RwLock::new(auth_flow), + tags: RwLock::new(tags), + safety_processes: RwLock::new(safety_processes), + file_watcher: RwLock::new(file_watcher), + })) } /// Updates state with data from the web @@ -240,7 +255,7 @@ impl State { } } -async fn init_watcher() -> crate::Result> { +pub async fn init_watcher() -> crate::Result> { let (mut tx, mut rx) = channel(1); let file_watcher = new_debouncer( @@ -256,13 +271,19 @@ async fn init_watcher() -> crate::Result> { tokio::task::spawn(async move { while let Some(res) = rx.next().await { match res { - Ok(events) => { + Ok(mut events) => { let mut visited_paths = Vec::new(); + // sort events by e.path + events.sort_by(|a, b| a.path.cmp(&b.path)); events.iter().for_each(|e| { + tracing::debug!( + "File watcher event: {:?}", + serde_json::to_string(&e.path).unwrap() + ); let mut new_path = PathBuf::new(); + let mut components_iterator = e.path.components(); let mut found = false; - - for component in e.path.components() { + for component in components_iterator.by_ref() { new_path.push(component); if found { break; @@ -271,6 +292,14 @@ async fn init_watcher() -> crate::Result> { found = true; } } + // if any remain, it's a subfile of the profile folder and not the profile folder itself + let subfile = components_iterator.next().is_some(); + + // At this point, new_path is the path to the profile, and subfile is whether it's a subfile of the profile or not + let profile_path_id = + ProfilePathId::new(&PathBuf::from( + new_path.file_name().unwrap_or_default(), + )); if e.path .components() @@ -280,10 +309,16 @@ async fn init_watcher() -> crate::Result> { .map(|x| x == "txt") .unwrap_or(false) { - Profile::crash_task(new_path); + Profile::crash_task(profile_path_id); } else if !visited_paths.contains(&new_path) { - Profile::sync_projects_task(new_path.clone()); - visited_paths.push(new_path); + if subfile { + Profile::sync_projects_task(profile_path_id); + visited_paths.push(new_path); + } else { + Profiles::sync_available_profiles_task( + profile_path_id, + ); + } } }); } diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 5fb62981..b36a67f4 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -28,7 +28,7 @@ use uuid::Uuid; const PROFILE_JSON_PATH: &str = "profile.json"; -pub(crate) struct Profiles(pub HashMap); +pub(crate) struct Profiles(pub HashMap); #[derive( Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq, @@ -46,13 +46,101 @@ pub enum ProfileInstallStage { NotInstalled, } +/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used +/// eg: for "a/b/c/profiles/My Mod", the ProfilePathId would be "My Mod" (a relative path) +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] +#[serde(transparent)] +pub struct ProfilePathId(PathBuf); +impl ProfilePathId { + // Create a new ProfilePathId from a full file path + pub async fn from_fs_path(path: PathBuf) -> crate::Result { + let path: PathBuf = io::canonicalize(path)?; + let profiles_dir = io::canonicalize( + State::get().await?.directories.profiles_dir().await, + )?; + path.strip_prefix(profiles_dir) + .ok() + .and_then(|p| p.file_name()) + .ok_or_else(|| { + crate::ErrorKind::FSError(format!( + "Path {path:?} does not correspond to a profile", + path = path + )) + })?; + Ok(Self(path)) + } + + // Create a new ProfilePathId from a relative path + pub fn new(path: &Path) -> Self { + ProfilePathId(PathBuf::from(path)) + } + + pub async fn get_full_path(&self) -> crate::Result { + let state = State::get().await?; + let profiles_dir = state.directories.profiles_dir().await; + Ok(profiles_dir.join(&self.0)) + } + + pub fn check_valid_utf(&self) -> crate::Result<&Self> { + self.0 + .to_str() + .ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?; + Ok(self) + } +} +impl std::fmt::Display for ProfilePathId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.display().fmt(f) + } +} + +/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used +/// eg: for "a/b/c/profiles/My Mod/mods/myproj", the ProjectPathId would be "mods/myproj" +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] +#[serde(transparent)] +pub struct ProjectPathId(pub PathBuf); +impl ProjectPathId { + // Create a new ProjectPathId from a full file path + pub async fn from_fs_path(path: PathBuf) -> crate::Result { + let path: PathBuf = io::canonicalize(path)?; + let profiles_dir: PathBuf = io::canonicalize( + State::get().await?.directories.profiles_dir().await, + )?; + path.strip_prefix(profiles_dir) + .ok() + .map(|p| p.components().skip(1).collect::()) + .ok_or_else(|| { + crate::ErrorKind::FSError(format!( + "Path {path:?} does not correspond to a profile", + path = path + )) + })?; + Ok(Self(path)) + } + + pub async fn get_full_path( + &self, + profile: ProfilePathId, + ) -> crate::Result { + let _state = State::get().await?; + let profile_dir = profile.get_full_path().await?; + Ok(profile_dir.join(&self.0)) + } + + // Create a new ProjectPathId from a relative path + pub fn new(path: &Path) -> Self { + ProjectPathId(PathBuf::from(path)) + } +} + // Represent a Minecraft instance. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Profile { pub uuid: Uuid, // todo: will be used in restructure to refer to profiles #[serde(default)] pub install_stage: ProfileInstallStage, - pub path: PathBuf, + #[serde(default)] + pub path: PathBuf, // Relative path to the profile, to be used in ProfilePathId pub metadata: ProfileMetadata, #[serde(skip_serializing_if = "Option::is_none")] pub java: Option, @@ -62,7 +150,7 @@ pub struct Profile { pub resolution: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hooks: Option, - pub projects: HashMap, + pub projects: HashMap, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -148,7 +236,6 @@ impl Profile { uuid: Uuid, name: String, version: String, - path: PathBuf, ) -> crate::Result { if name.trim().is_empty() { return Err(crate::ErrorKind::InputError(String::from( @@ -160,7 +247,7 @@ impl Profile { Ok(Self { uuid, install_stage: ProfileInstallStage::NotInstalled, - path: io::canonicalize(path)?, + path: PathBuf::new().join(&name), metadata: ProfileMetadata { name, icon: None, @@ -182,6 +269,12 @@ impl Profile { }) } + // Gets the ProfilePathId for this profile + #[inline] + pub fn profile_id(&self) -> ProfilePathId { + ProfilePathId::new(&self.path) + } + #[tracing::instrument(skip(self, semaphore, icon))] pub async fn set_icon<'a>( &'a mut self, @@ -197,7 +290,7 @@ impl Profile { Ok(()) } - pub fn crash_task(path: PathBuf) { + pub fn crash_task(path: ProfilePathId) { tokio::task::spawn(async move { let res = async { let profile = crate::api::profile::get(&path, None).await?; @@ -221,38 +314,47 @@ impl Profile { }); } - pub fn sync_projects_task(path: PathBuf) { + pub fn sync_projects_task(profile_path_id: ProfilePathId) { tokio::task::spawn(async move { + let span = + tracing::span!(tracing::Level::INFO, "sync_projects_task"); + tracing::debug!( + parent: &span, + "Syncing projects for profile {}", + profile_path_id + ); let res = async { + let _span = span.enter(); let state = State::get().await?; - let profile = crate::api::profile::get(&path, None).await?; + let profile = crate::api::profile::get(&profile_path_id, None).await?; if let Some(profile) = profile { - let paths = profile.get_profile_project_paths()?; + let paths = profile.get_profile_full_project_paths().await?; + let caches_dir = state.directories.caches_dir(); let projects = crate::state::infer_data_from_files( profile.clone(), paths, - state.directories.caches_dir(), + caches_dir, &state.io_semaphore, &state.fetch_semaphore, ) .await?; let mut new_profiles = state.profiles.write().await; - if let Some(profile) = new_profiles.0.get_mut(&path) { + if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) { profile.projects = projects; } emit_profile( profile.uuid, - profile.path, + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Synced, ) .await?; } else { tracing::warn!( - "Unable to fetch single profile projects: path {path:?} invalid", + "Unable to fetch single profile projects: path {profile_path_id} invalid", ); } Ok::<(), crate::Error>(()) @@ -268,10 +370,21 @@ impl Profile { }); } - pub fn get_profile_project_paths(&self) -> crate::Result> { + // Get full path to profile + pub async fn get_profile_full_path(&self) -> crate::Result { + let state = State::get().await?; + let profiles_dir = state.directories.profiles_dir().await; + Ok(profiles_dir.join(&self.path)) + } + + /// Gets paths to projects as their full paths, not just their relative paths + pub async fn get_profile_full_project_paths( + &self, + ) -> crate::Result> { let mut files = Vec::new(); + let profile_path = self.get_profile_full_path().await?; let mut read_paths = |path: &str| { - let new_path = self.path.join(path); + let new_path = profile_path.join(path); if new_path.exists() { let path = self.path.join(path); for path in std::fs::read_dir(&path) @@ -338,7 +451,7 @@ impl Profile { pub async fn add_project_version( &self, version_id: String, - ) -> crate::Result<(PathBuf, ModrinthVersion)> { + ) -> crate::Result<(ProjectPathId, ModrinthVersion)> { let state = State::get().await?; let version = fetch_json::( @@ -387,7 +500,7 @@ impl Profile { file_name: &str, bytes: bytes::Bytes, project_type: Option, - ) -> crate::Result { + ) -> crate::Result { let project_type = if let Some(project_type) = project_type { project_type } else { @@ -419,16 +532,23 @@ impl Profile { }; let state = State::get().await?; - let path = self.path.join(project_type.get_folder()).join(file_name); - write(&path, &bytes, &state.io_semaphore).await?; + let relative_name = PathBuf::new() + .join(project_type.get_folder()) + .join(file_name); + let file_path = self + .get_profile_full_path() + .await? + .join(relative_name.clone()); + let project_path_id = ProjectPathId::new(&relative_name); + write(&file_path, &bytes, &state.io_semaphore).await?; let hash = get_hash(bytes).await?; { let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(&self.path) { + if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { profile.projects.insert( - path.clone(), + project_path_id.clone(), Project { sha512: hash, disabled: false, @@ -440,32 +560,40 @@ impl Profile { } } - Ok(path) + Ok(project_path_id) } + /// Toggle a project's disabled state. + /// 'path' should be relative to the profile's path. #[tracing::instrument(skip(self))] #[theseus_macros::debug_pin] pub async fn toggle_disable_project( &self, - path: &Path, - ) -> crate::Result { + relative_path: &ProjectPathId, + ) -> crate::Result { let state = State::get().await?; if let Some(mut project) = { - let mut profiles = state.profiles.write().await; + let mut profiles: tokio::sync::RwLockWriteGuard<'_, Profiles> = + state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(&self.path) { - profile.projects.remove(path) + if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { + profile.projects.remove(relative_path) } else { None } } { - let path = path.to_path_buf(); - let mut new_path = path.clone(); + // Get relative path from former ProjectPathId + let relative_path = relative_path.0.to_path_buf(); + let mut new_path = relative_path.clone(); - if path.extension().map_or(false, |ext| ext == "disabled") { + if relative_path + .extension() + .map_or(false, |ext| ext == "disabled") + { project.disabled = false; new_path.set_file_name( - path.file_name() + relative_path + .file_name() .unwrap_or_default() .to_string_lossy() .replace(".disabled", ""), @@ -473,24 +601,35 @@ impl Profile { } else { new_path.set_file_name(format!( "{}.disabled", - path.file_name().unwrap_or_default().to_string_lossy() + relative_path + .file_name() + .unwrap_or_default() + .to_string_lossy() )); project.disabled = true; } - io::rename(&path, &new_path).await?; + let true_path = + self.get_profile_full_path().await?.join(&relative_path); + let true_new_path = + self.get_profile_full_path().await?.join(&new_path); + io::rename(&true_path, &true_new_path).await?; + + let new_project_path_id = ProjectPathId::new(&new_path); let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(&self.path) { - profile.projects.insert(new_path.clone(), project); + if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { + profile + .projects + .insert(new_project_path_id.clone(), project); profile.metadata.date_modified = Utc::now(); } - Ok(new_path) + Ok(new_project_path_id) } else { Err(crate::ErrorKind::InputError(format!( "Project path does not exist: {:?}", - path + relative_path )) .into()) } @@ -498,24 +637,29 @@ impl Profile { pub async fn remove_project( &self, - path: &Path, + relative_path: &ProjectPathId, dont_remove_arr: Option, ) -> crate::Result<()> { let state = State::get().await?; - if self.projects.contains_key(path) { - io::remove_file(path).await?; + if self.projects.contains_key(relative_path) { + io::remove_file( + self.get_profile_full_path() + .await? + .join(relative_path.0.clone()), + ) + .await?; if !dont_remove_arr.unwrap_or(false) { let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(&self.path) { - profile.projects.remove(path); + if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { + profile.projects.remove(relative_path); profile.metadata.date_modified = Utc::now(); } } } else { return Err(crate::ErrorKind::InputError(format!( "Project path does not exist: {:?}", - path + relative_path )) .into()); } @@ -532,14 +676,21 @@ impl Profiles { file_watcher: &mut Debouncer, ) -> crate::Result { let mut profiles = HashMap::new(); - io::create_dir_all(&dirs.profiles_dir()).await?; - let mut entries = io::read_dir(&dirs.profiles_dir()).await?; + let profiles_dir = dirs.profiles_dir().await; + io::create_dir_all(&&profiles_dir).await?; + + file_watcher + .watcher() + .watch(&profiles_dir, RecursiveMode::NonRecursive)?; + + let mut entries = io::read_dir(&dirs.profiles_dir().await).await?; while let Some(entry) = entries.next_entry().await.map_err(IOError::from)? { let path = entry.path(); if path.is_dir() { - let prof = match Self::read_profile_from_dir(&path).await { + let prof = match Self::read_profile_from_dir(&path, dirs).await + { Ok(prof) => Some(prof), Err(err) => { tracing::warn!( @@ -551,7 +702,7 @@ impl Profiles { if let Some(profile) = prof { let path = io::canonicalize(path)?; Profile::watch_fs(&path, file_watcher).await?; - profiles.insert(path, profile); + profiles.insert(profile.profile_id(), profile); } } } @@ -570,26 +721,28 @@ impl Profiles { { let profiles = state.profiles.read().await; for (_profile_path, profile) in profiles.0.iter() { - let paths = profile.get_profile_project_paths()?; + let paths = + profile.get_profile_full_project_paths().await?; files.push((profile.clone(), paths)); } } + let caches_dir = state.directories.caches_dir(); future::try_join_all(files.into_iter().map( |(profile, files)| async { - let profile_path = profile.path.clone(); + let profile_name = profile.profile_id(); let inferred = super::projects::infer_data_from_files( profile, files, - state.directories.caches_dir(), + caches_dir.clone(), &state.io_semaphore, &state.fetch_semaphore, ) .await?; let mut new_profiles = state.profiles.write().await; - if let Some(profile) = new_profiles.0.get_mut(&profile_path) + if let Some(profile) = new_profiles.0.get_mut(&profile_name) { profile.projects = inferred; } @@ -622,7 +775,7 @@ impl Profiles { pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> { emit_profile( profile.uuid, - profile.path.clone(), + profile.get_profile_full_path().await?, &profile.metadata.name, ProfilePayloadType::Added, ) @@ -630,30 +783,26 @@ impl Profiles { let state = State::get().await?; let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs(&profile.path, &mut file_watcher).await?; + Profile::watch_fs( + &profile.get_profile_full_path().await?, + &mut file_watcher, + ) + .await?; - self.0.insert( - io::canonicalize(&profile.path)? - .to_str() - .ok_or( - crate::ErrorKind::UTFError(profile.path.clone()).as_error(), - )? - .into(), - profile, - ); + let profile_name = profile.profile_id(); + profile_name.check_valid_utf()?; + self.0.insert(profile_name, profile); Ok(self) } #[tracing::instrument(skip(self))] pub async fn remove( &mut self, - path: &Path, + profile_path: &ProfilePathId, ) -> crate::Result> { - let path = PathBuf::from( - &io::canonicalize(path)?.to_string_lossy().to_string(), - ); - let profile = self.0.remove(&path); + let profile = self.0.remove(profile_path); + let path = profile_path.get_full_path().await?; if path.exists() { io::remove_dir_all(&path).await?; } @@ -663,12 +812,15 @@ impl Profiles { #[tracing::instrument(skip_all)] pub async fn sync(&self) -> crate::Result<&Self> { + let _state = State::get().await?; stream::iter(self.0.iter()) .map(Ok::<_, crate::Error>) - .try_for_each_concurrent(None, |(path, profile)| async move { + .try_for_each_concurrent(None, |(_, profile)| async move { let json = serde_json::to_vec(&profile)?; - let json_path = Path::new(&path.to_string_lossy().to_string()) + let json_path = profile + .get_profile_full_path() + .await? .join(PROFILE_JSON_PATH); io::write(&json_path, &json).await?; @@ -679,10 +831,68 @@ impl Profiles { Ok(self) } - async fn read_profile_from_dir(path: &Path) -> crate::Result { + async fn read_profile_from_dir( + path: &Path, + dirs: &DirectoryInfo, + ) -> crate::Result { let json = io::read(&path.join(PROFILE_JSON_PATH)).await?; let mut profile = serde_json::from_slice::(&json)?; - profile.path = PathBuf::from(path); + + // Get name from stripped path + profile.path = + PathBuf::from(path.strip_prefix(dirs.profiles_dir().await)?); + Ok(profile) } + + pub fn sync_available_profiles_task(profile_path_id: ProfilePathId) { + tokio::task::spawn(async move { + let span = tracing::span!( + tracing::Level::INFO, + "sync_available_profiles_task" + ); + let res = async { + let _span = span.enter(); + let state = State::get().await?; + let dirs = &state.directories; + let mut profiles = state.profiles.write().await; + + if let Some(profile) = profiles.0.get_mut(&profile_path_id) { + if !profile.get_profile_full_path().await?.exists() { + // if path exists in the state but no longer in the filesystem, remove it from the state list + emit_profile( + profile.uuid, + profile.get_profile_full_path().await?, + &profile.metadata.name, + ProfilePayloadType::Removed, + ) + .await?; + tracing::debug!("Removed!"); + profiles.0.remove(&profile_path_id); + } + } else if profile_path_id.get_full_path().await?.exists() { + // if it exists in the filesystem but no longer in the state, add it to the state list + profiles + .insert( + Self::read_profile_from_dir( + &profile_path_id.get_full_path().await?, + dirs, + ) + .await?, + ) + .await?; + Profile::sync_projects_task(profile_path_id); + } + Ok::<(), crate::Error>(()) + } + .await; + + match res { + Ok(()) => {} + Err(err) => { + tracing::warn!("Unable to fetch all profiles: {err}") + } + }; + }); + } } diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index 2a81288d..dd380214 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -8,6 +8,7 @@ use crate::util::fetch::{ use crate::util::io::IOError; use async_zip::tokio::read::fs::ZipFileReader; use chrono::{DateTime, Utc}; +use futures::StreamExt; use reqwest::Method; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -16,6 +17,8 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use tokio::io::AsyncReadExt; +use super::ProjectPathId; + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] pub enum ProjectType { @@ -253,6 +256,9 @@ async fn read_icon_from_file( Ok(None) } +// Creates Project data from the existing files in the file system, for a given Profile +// Paths must be the full paths to the files in the FS, and not the relative paths +// eg: with get_profile_full_project_paths #[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))] #[theseus_macros::debug_pin] pub async fn infer_data_from_files( @@ -261,7 +267,7 @@ pub async fn infer_data_from_files( cache_dir: PathBuf, io_semaphore: &IoSemaphore, fetch_semaphore: &FetchSemaphore, -) -> crate::Result> { +) -> crate::Result> { let mut file_path_hashes = HashMap::new(); // TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory @@ -342,7 +348,7 @@ pub async fn infer_data_from_files( .flatten() .collect(); - let mut return_projects = HashMap::new(); + let mut return_projects: Vec<(PathBuf, Project)> = Vec::new(); let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new(); for (hash, path) in file_path_hashes { @@ -356,7 +362,7 @@ pub async fn infer_data_from_files( .to_string_lossy() .to_string(); - return_projects.insert( + return_projects.push(( path, Project { disabled: file_name.ends_with(".disabled"), @@ -392,7 +398,7 @@ pub async fn infer_data_from_files( sha512: hash, file_name, }, - ); + )); continue; } } @@ -412,7 +418,7 @@ pub async fn infer_data_from_files( { zip_file_reader } else { - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -420,7 +426,7 @@ pub async fn infer_data_from_files( metadata: ProjectMetadata::Unknown, file_name, }, - ); + )); continue; }; let zip_index_option = zip_file_reader @@ -466,7 +472,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -491,7 +497,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -533,7 +539,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -552,7 +558,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -599,7 +605,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -621,7 +627,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -665,7 +671,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -697,7 +703,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -731,7 +737,7 @@ pub async fn infer_data_from_files( io_semaphore, ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -746,13 +752,13 @@ pub async fn infer_data_from_files( project_type: None, }, }, - ); + )); continue; } } } - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -760,8 +766,17 @@ pub async fn infer_data_from_files( file_name, metadata: ProjectMetadata::Unknown, }, - ); + )); } - Ok(return_projects) + // Project paths should be relative + let _profile_base_path = profile.get_profile_full_path().await?; + let mut corrected_hashmap = HashMap::new(); + let mut stream = tokio_stream::iter(return_projects); + while let Some((h, v)) = stream.next().await { + let h = ProjectPathId::from_fs_path(h).await?; + corrected_hashmap.insert(h, v); + } + + Ok(corrected_hashmap) } diff --git a/theseus/src/state/settings.rs b/theseus/src/state/settings.rs index c1de1069..4d17e5b1 100644 --- a/theseus/src/state/settings.rs +++ b/theseus/src/state/settings.rs @@ -4,10 +4,10 @@ use crate::{ State, }; use serde::{Deserialize, Serialize}; -use std::path::Path; +use std::path::{Path, PathBuf}; use tokio::fs; -use super::JavaGlobals; +use super::{DirectoryInfo, JavaGlobals}; // TODO: convert to semver? const CURRENT_FORMAT_VERSION: u32 = 1; @@ -15,7 +15,6 @@ const CURRENT_FORMAT_VERSION: u32 = 1; // Types /// Global Theseus settings #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(default)] pub struct Settings { pub theme: Theme, pub memory: MemorySettings, @@ -41,31 +40,8 @@ pub struct Settings { pub advanced_rendering: bool, #[serde(default)] pub onboarded: bool, -} - -impl Default for Settings { - fn default() -> Self { - Self { - theme: Theme::Dark, - memory: MemorySettings::default(), - game_resolution: WindowSize::default(), - custom_java_args: Vec::new(), - custom_env_args: Vec::new(), - java_globals: JavaGlobals::new(), - default_user: None, - hooks: Hooks::default(), - max_concurrent_downloads: 10, - max_concurrent_writes: 10, - version: CURRENT_FORMAT_VERSION, - collapsed_navigation: false, - hide_on_process: false, - default_page: DefaultPage::Home, - developer_mode: false, - opt_out_analytics: false, - advanced_rendering: true, - onboarded: false, - } - } + #[serde(default = "DirectoryInfo::get_initial_settings_dir")] + pub loaded_config_dir: Option, } impl Settings { @@ -85,7 +61,29 @@ impl Settings { .map_err(crate::Error::from) }) } else { - Ok(Settings::default()) + Ok(Self { + theme: Theme::Dark, + memory: MemorySettings::default(), + game_resolution: WindowSize::default(), + custom_java_args: Vec::new(), + custom_env_args: Vec::new(), + java_globals: JavaGlobals::new(), + default_user: None, + hooks: Hooks::default(), + max_concurrent_downloads: 10, + max_concurrent_writes: 10, + version: CURRENT_FORMAT_VERSION, + collapsed_navigation: false, + hide_on_process: false, + default_page: DefaultPage::Home, + developer_mode: false, + opt_out_analytics: false, + advanced_rendering: true, + onboarded: false, + + // By default, the config directory is the same as the settings directory + loaded_config_dir: DirectoryInfo::get_initial_settings_dir(), + }) } } diff --git a/theseus/src/state/tags.rs b/theseus/src/state/tags.rs index dab2762c..01ce8c45 100644 --- a/theseus/src/state/tags.rs +++ b/theseus/src/state/tags.rs @@ -28,7 +28,7 @@ impl Tags { fetch_semaphore: &FetchSemaphore, ) -> crate::Result { let mut tags = None; - let tags_path = dirs.caches_meta_dir().join("tags.json"); + let tags_path = dirs.caches_meta_dir().await.join("tags.json"); if let Ok(tags_json) = read_json::(&tags_path, io_semaphore).await { @@ -60,7 +60,7 @@ impl Tags { let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?; let tags_path = - state.directories.caches_meta_dir().join("tags.json"); + state.directories.caches_meta_dir().await.join("tags.json"); write( &tags_path, diff --git a/theseus/src/state/users.rs b/theseus/src/state/users.rs index 6d402a6d..d88b4d04 100644 --- a/theseus/src/state/users.rs +++ b/theseus/src/state/users.rs @@ -17,7 +17,7 @@ impl Users { dirs: &DirectoryInfo, io_semaphore: &IoSemaphore, ) -> crate::Result { - let users_path = dirs.caches_meta_dir().join(USERS_JSON); + let users_path = dirs.caches_meta_dir().await.join(USERS_JSON); let users = read_json(&users_path, io_semaphore).await.ok(); if let Some(users) = users { @@ -29,7 +29,8 @@ impl Users { pub async fn save(&self) -> crate::Result<()> { let state = State::get().await?; - let users_path = state.directories.caches_meta_dir().join(USERS_JSON); + let users_path = + state.directories.caches_meta_dir().await.join(USERS_JSON); write( &users_path, &serde_json::to_vec(&self.0)?, diff --git a/theseus/src/util/fetch.rs b/theseus/src/util/fetch.rs index ef17fc20..97cd01cb 100644 --- a/theseus/src/util/fetch.rs +++ b/theseus/src/util/fetch.rs @@ -221,6 +221,7 @@ pub async fn write<'a>( Ok(()) } +// Writes a icon to the cache and returns the absolute path of the icon within the cache directory #[tracing::instrument(skip(bytes, semaphore))] pub async fn write_cached_icon( icon_path: &str, diff --git a/theseus/src/util/jre.rs b/theseus/src/util/jre.rs index 986e4780..46245e5d 100644 --- a/theseus/src/util/jre.rs +++ b/theseus/src/util/jre.rs @@ -201,7 +201,7 @@ async fn get_all_autoinstalled_jre_path() -> Result, JREError> let state = State::get().await.map_err(|_| JREError::StateError)?; let mut jre_paths = HashSet::new(); - let base_path = state.directories.java_versions_dir(); + let base_path = state.directories.java_versions_dir().await; if base_path.is_dir() { if let Ok(dir) = std::fs::read_dir(base_path) { diff --git a/theseus_cli/src/subcommands/profile.rs b/theseus_cli/src/subcommands/profile.rs index 92a92abf..a2c773f9 100644 --- a/theseus_cli/src/subcommands/profile.rs +++ b/theseus_cli/src/subcommands/profile.rs @@ -227,7 +227,7 @@ impl<'a> From<&'a Profile> for ProfileRow<'a> { fn from(it: &'a Profile) -> Self { Self { name: &it.metadata.name, - path: &it.path, + path: Path::new(&it.metadata.name), game_version: &it.metadata.game_version, loader: &it.metadata.loader, loader_version: it @@ -285,7 +285,8 @@ impl ProfileRemove { _args: &crate::Args, _largs: &ProfileCommand, ) -> Result<()> { - let profile = canonicalize(&self.profile)?; + let profile = + ProfilePathId::from_fs_path(canonicalize(&self.profile)?).await?; info!("Removing profile {} from Theseus", self.profile.display()); if confirm_async(String::from("Do you wish to continue"), true).await? { @@ -335,7 +336,9 @@ impl ProfileRun { .await?; let credentials = auth::refresh(id).await?; - let proc_lock = profile::run_credentials(&path, &credentials).await?; + let profile_path_id = ProfilePathId::from_fs_path(path).await?; + let proc_lock = + profile::run_credentials(&profile_path_id, &credentials).await?; let mut proc = proc_lock.write().await; process::wait_for(&mut proc).await?; diff --git a/theseus_cli/src/subcommands/user.rs b/theseus_cli/src/subcommands/user.rs index e866b982..a6e7ec64 100644 --- a/theseus_cli/src/subcommands/user.rs +++ b/theseus_cli/src/subcommands/user.rs @@ -151,7 +151,7 @@ impl UserDefault { ) -> Result<()> { info!("Setting user {} as default", self.user.as_hyphenated()); - let state: std::sync::Arc = State::get().await?; + let state = State::get().await?; let mut settings = state.settings.write().await; if settings.default_user == Some(self.user) { diff --git a/theseus_gui/src-tauri/src/api/logs.rs b/theseus_gui/src-tauri/src/api/logs.rs index c81dbe03..1cb40f7d 100644 --- a/theseus_gui/src-tauri/src/api/logs.rs +++ b/theseus_gui/src-tauri/src/api/logs.rs @@ -50,7 +50,18 @@ pub async fn logs_get_output_by_datetime( profile_uuid: Uuid, datetime_string: String, ) -> Result { - Ok(logs::get_output_by_datetime(profile_uuid, &datetime_string).await?) + let profile_path = if let Some(p) = + crate::profile::get_by_uuid(profile_uuid, None).await? + { + p.profile_id() + } else { + return Err(theseus::Error::from( + theseus::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()), + ) + .into()); + }; + + Ok(logs::get_output_by_datetime(&profile_path, &datetime_string).await?) } /// Delete all logs for a profile by profile id diff --git a/theseus_gui/src-tauri/src/api/pack.rs b/theseus_gui/src-tauri/src/api/pack.rs index 1d13d8a1..eb6e4212 100644 --- a/theseus_gui/src-tauri/src/api/pack.rs +++ b/theseus_gui/src-tauri/src/api/pack.rs @@ -1,5 +1,5 @@ use crate::api::Result; -use std::path::PathBuf; + use theseus::{ pack::{ install::install_pack, @@ -20,8 +20,8 @@ pub fn init() -> tauri::plugin::TauriPlugin { #[tauri::command] pub async fn pack_install( location: CreatePackLocation, - profile: PathBuf, -) -> Result { + profile: ProfilePathId, +) -> Result { Ok(install_pack(location, profile).await?) } diff --git a/theseus_gui/src-tauri/src/api/process.rs b/theseus_gui/src-tauri/src/api/process.rs index 58ffb8e3..714855ff 100644 --- a/theseus_gui/src-tauri/src/api/process.rs +++ b/theseus_gui/src-tauri/src/api/process.rs @@ -1,5 +1,3 @@ -use std::path::{Path, PathBuf}; - use crate::api::Result; use theseus::prelude::*; use uuid::Uuid; @@ -50,14 +48,15 @@ pub async fn process_get_all_running_uuids() -> Result> { // Gets all process UUIDs by profile path #[tauri::command] pub async fn process_get_uuids_by_profile_path( - profile_path: &Path, + profile_path: ProfilePathId, ) -> Result> { Ok(process::get_uuids_by_profile_path(profile_path).await?) } // Gets the Profile paths of each *running* stored process in the state #[tauri::command] -pub async fn process_get_all_running_profile_paths() -> Result> { +pub async fn process_get_all_running_profile_paths( +) -> Result> { Ok(process::get_all_running_profile_paths().await?) } diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index c2650932..a2642ae6 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -36,8 +36,8 @@ pub fn init() -> tauri::plugin::TauriPlugin { // Remove a profile // invoke('plugin:profile|profile_add_path',path) #[tauri::command] -pub async fn profile_remove(path: &Path) -> Result<()> { - profile::remove(path).await?; +pub async fn profile_remove(path: ProfilePathId) -> Result<()> { + profile::remove(&path).await?; Ok(()) } @@ -45,19 +45,19 @@ pub async fn profile_remove(path: &Path) -> Result<()> { // invoke('plugin:profile|profile_add_path',path) #[tauri::command] pub async fn profile_get( - path: &Path, + path: ProfilePathId, clear_projects: Option, ) -> Result> { - let res = profile::get(path, clear_projects).await?; + let res = profile::get(&path, clear_projects).await?; Ok(res) } // Get optimal java version from profile #[tauri::command] pub async fn profile_get_optimal_jre_key( - path: &Path, + path: ProfilePathId, ) -> Result> { - let res = profile::get_optimal_jre_key(path).await?; + let res = profile::get_optimal_jre_key(&path).await?; Ok(res) } @@ -66,14 +66,14 @@ pub async fn profile_get_optimal_jre_key( #[tauri::command] pub async fn profile_list( clear_projects: Option, -) -> Result> { +) -> Result> { let res = profile::list(clear_projects).await?; Ok(res) } #[tauri::command] pub async fn profile_check_installed( - path: &Path, + path: ProfilePathId, project_id: String, ) -> Result { let profile = profile_get(path, None).await?; @@ -94,8 +94,8 @@ pub async fn profile_check_installed( /// Installs/Repairs a profile /// invoke('plugin:profile|profile_install') #[tauri::command] -pub async fn profile_install(path: &Path) -> Result<()> { - profile::install(path).await?; +pub async fn profile_install(path: ProfilePathId) -> Result<()> { + profile::install(&path).await?; Ok(()) } @@ -103,40 +103,40 @@ pub async fn profile_install(path: &Path) -> Result<()> { /// invoke('plugin:profile|profile_update_all') #[tauri::command] pub async fn profile_update_all( - path: &Path, -) -> Result> { - Ok(profile::update_all(path).await?) + path: ProfilePathId, +) -> Result> { + Ok(profile::update_all(&path).await?) } /// Updates a specified project /// invoke('plugin:profile|profile_update_project') #[tauri::command] pub async fn profile_update_project( - path: &Path, - project_path: &Path, -) -> Result { - Ok(profile::update_project(path, project_path, None).await?) + path: ProfilePathId, + project_path: ProjectPathId, +) -> Result { + Ok(profile::update_project(&path, &project_path, None).await?) } // Adds a project to a profile from a version ID // invoke('plugin:profile|profile_add_project_from_version') #[tauri::command] pub async fn profile_add_project_from_version( - path: &Path, + path: ProfilePathId, version_id: String, -) -> Result { - Ok(profile::add_project_from_version(path, version_id).await?) +) -> Result { + Ok(profile::add_project_from_version(&path, version_id).await?) } // Adds a project to a profile from a path // invoke('plugin:profile|profile_add_project_from_path') #[tauri::command] pub async fn profile_add_project_from_path( - path: &Path, + path: ProfilePathId, project_path: &Path, project_type: Option, -) -> Result { - let res = profile::add_project_from_path(path, project_path, project_type) +) -> Result { + let res = profile::add_project_from_path(&path, project_path, project_type) .await?; Ok(res) } @@ -145,20 +145,20 @@ pub async fn profile_add_project_from_path( // invoke('plugin:profile|profile_toggle_disable_project') #[tauri::command] pub async fn profile_toggle_disable_project( - path: &Path, - project_path: &Path, -) -> Result { - Ok(profile::toggle_disable_project(path, project_path).await?) + path: ProfilePathId, + project_path: ProjectPathId, +) -> Result { + Ok(profile::toggle_disable_project(&path, &project_path).await?) } // Removes a project from a profile // invoke('plugin:profile|profile_remove_project') #[tauri::command] pub async fn profile_remove_project( - path: &Path, - project_path: &Path, + path: ProfilePathId, + project_path: ProjectPathId, ) -> Result<()> { - profile::remove_project(path, project_path).await?; + profile::remove_project(&path, &project_path).await?; Ok(()) } @@ -166,13 +166,13 @@ pub async fn profile_remove_project( // invoke('profile_export_mrpack') #[tauri::command] pub async fn profile_export_mrpack( - path: &Path, + path: ProfilePathId, export_location: PathBuf, included_overrides: Vec, version_id: Option, ) -> Result<()> { profile::export_mrpack( - path, + &path, export_location, included_overrides, version_id, @@ -190,7 +190,7 @@ pub async fn profile_export_mrpack( // => [folder1, folder2] #[tauri::command] pub async fn profile_get_potential_override_folders( - profile_path: PathBuf, + profile_path: ProfilePathId, ) -> Result> { let overrides = profile::get_potential_override_folders(profile_path).await?; @@ -202,8 +202,8 @@ pub async fn profile_get_potential_override_folders( // for the actual Child in the state. // invoke('plugin:profile|profile_run', path) #[tauri::command] -pub async fn profile_run(path: &Path) -> Result { - let minecraft_child = profile::run(path).await?; +pub async fn profile_run(path: ProfilePathId) -> Result { + let minecraft_child = profile::run(&path).await?; let uuid = minecraft_child.read().await.uuid; Ok(uuid) } @@ -211,8 +211,8 @@ pub async fn profile_run(path: &Path) -> Result { // Run Minecraft using a profile using the default credentials, and wait for the result // invoke('plugin:profile|profile_run_wait', path) #[tauri::command] -pub async fn profile_run_wait(path: &Path) -> Result<()> { - let proc_lock = profile::run(path).await?; +pub async fn profile_run_wait(path: ProfilePathId) -> Result<()> { + let proc_lock = profile::run(&path).await?; let mut proc = proc_lock.write().await; Ok(process::wait_for(&mut proc).await?) } @@ -223,11 +223,12 @@ pub async fn profile_run_wait(path: &Path) -> Result<()> { // invoke('plugin:profile|profile_run_credentials', {path, credentials})') #[tauri::command] pub async fn profile_run_credentials( - path: &Path, + path: ProfilePathId, credentials: Credentials, ) -> Result { - let minecraft_child = profile::run_credentials(path, &credentials).await?; + let minecraft_child = profile::run_credentials(&path, &credentials).await?; let uuid = minecraft_child.read().await.uuid; + Ok(uuid) } @@ -235,10 +236,10 @@ pub async fn profile_run_credentials( // invoke('plugin:profile|profile_run_wait', {path, credentials) #[tauri::command] pub async fn profile_run_wait_credentials( - path: &Path, + path: ProfilePathId, credentials: Credentials, ) -> Result<()> { - let proc_lock = profile::run_credentials(path, &credentials).await?; + let proc_lock = profile::run_credentials(&path, &credentials).await?; let mut proc = proc_lock.write().await; Ok(process::wait_for(&mut proc).await?) } @@ -265,10 +266,10 @@ pub struct EditProfileMetadata { // invoke('plugin:profile|profile_edit', {path, editProfile}) #[tauri::command] pub async fn profile_edit( - path: &Path, + path: ProfilePathId, edit_profile: EditProfile, ) -> Result<()> { - profile::edit(path, |prof| { + profile::edit(&path, |prof| { if let Some(metadata) = edit_profile.metadata.clone() { if let Some(name) = metadata.name { prof.metadata.name = name; @@ -305,9 +306,9 @@ pub async fn profile_edit( // invoke('plugin:profile|profile_edit_icon') #[tauri::command] pub async fn profile_edit_icon( - path: &Path, + path: ProfilePathId, icon_path: Option<&Path>, ) -> Result<()> { - profile::edit_icon(path, icon_path).await?; + profile::edit_icon(&path, icon_path).await?; Ok(()) } diff --git a/theseus_gui/src-tauri/src/api/profile_create.rs b/theseus_gui/src-tauri/src/api/profile_create.rs index 8afb4f77..03e363e5 100644 --- a/theseus_gui/src-tauri/src/api/profile_create.rs +++ b/theseus_gui/src-tauri/src/api/profile_create.rs @@ -17,7 +17,7 @@ pub async fn profile_create( modloader: ModLoader, // the modloader to use loader_version: Option, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader icon: Option, // the icon for the profile -) -> Result { +) -> Result { let res = profile_create::profile_create( name, game_version, diff --git a/theseus_gui/src-tauri/src/api/settings.rs b/theseus_gui/src-tauri/src/api/settings.rs index 9b90cf80..1b8b3c13 100644 --- a/theseus_gui/src-tauri/src/api/settings.rs +++ b/theseus_gui/src-tauri/src/api/settings.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use crate::api::Result; use serde::{Deserialize, Serialize}; use theseus::prelude::*; @@ -22,7 +24,11 @@ pub struct FrontendSettings { pub fn init() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new("settings") - .invoke_handler(tauri::generate_handler![settings_get, settings_set,]) + .invoke_handler(tauri::generate_handler![ + settings_get, + settings_set, + settings_change_config_dir + ]) .build() } @@ -41,3 +47,12 @@ pub async fn settings_set(settings: Settings) -> Result<()> { settings::set(settings).await?; Ok(()) } + +// Change config directory +// Seizes the entire State to do it +// invoke('plugin:settings|settings_change_config_dir', new_dir) +#[tauri::command] +pub async fn settings_change_config_dir(new_config_dir: PathBuf) -> Result<()> { + settings::set_config_dir(new_config_dir).await?; + Ok(()) +} diff --git a/theseus_gui/src-tauri/src/api/utils.rs b/theseus_gui/src-tauri/src/api/utils.rs index e3dfcb25..cef1bbc9 100644 --- a/theseus_gui/src-tauri/src/api/utils.rs +++ b/theseus_gui/src-tauri/src/api/utils.rs @@ -122,6 +122,6 @@ pub async fn handle_command(command: String) -> Result<()> { #[tauri::command] pub async fn await_sync() -> Result<()> { State::sync().await?; - tracing::info!("State synced"); + tracing::debug!("State synced"); Ok(()) } diff --git a/theseus_gui/src/components/ui/ExportModal.vue b/theseus_gui/src/components/ui/ExportModal.vue index 15a1065d..239e57b2 100644 --- a/theseus_gui/src/components/ui/ExportModal.vue +++ b/theseus_gui/src/components/ui/ExportModal.vue @@ -69,14 +69,12 @@ const exportPack = async () => { } }) }) - console.log(filesToExport) const outputPath = await open({ directory: true, multiple: false, }) if (outputPath) { - console.log(outputPath) export_profile_mrpack( props.instance.path, outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`, diff --git a/theseus_gui/src/components/ui/InstanceInstallModal.vue b/theseus_gui/src/components/ui/InstanceInstallModal.vue index a0b3a3c1..1534d05d 100644 --- a/theseus_gui/src/components/ui/InstanceInstallModal.vue +++ b/theseus_gui/src/components/ui/InstanceInstallModal.vue @@ -18,14 +18,13 @@ import { get, list, } from '@/helpers/profile' -import { tauri } from '@tauri-apps/api' import { open } from '@tauri-apps/api/dialog' -import { convertFileSrc } from '@tauri-apps/api/tauri' import { create } from '@/helpers/profile' import { installVersionDependencies } from '@/helpers/utils' import { handleError } from '@/store/notifications.js' import mixpanel from 'mixpanel-browser' import { useTheming } from '@/store/theme.js' +import { tauri } from '@tauri-apps/api' const themeStore = useTheming() @@ -227,7 +226,7 @@ const check_valid = computed(() => { !profile.metadata.icon || (profile.metadata.icon && profile.metadata.icon.startsWith('http')) ? profile.metadata.icon - : convertFileSrc(profile.metadata?.icon) + : tauri.convertFileSrc(profile.metadata?.icon) " class="profile-image" /> diff --git a/theseus_gui/src/helpers/settings.js b/theseus_gui/src/helpers/settings.js index 44495cb2..62b53745 100644 --- a/theseus_gui/src/helpers/settings.js +++ b/theseus_gui/src/helpers/settings.js @@ -37,3 +37,9 @@ export async function get() { export async function set(settings) { return await invoke('plugin:settings|settings_set', { settings }) } + +// Changes the config dir +// Seizes the entire application state until its done +export async function change_config_dir(newConfigDir) { + return await invoke('plugin:settings|settings_change_config_dir', { newConfigDir }) +} diff --git a/theseus_gui/src/pages/instance/Index.vue b/theseus_gui/src/pages/instance/Index.vue index f479b1d4..fd6a1072 100644 --- a/theseus_gui/src/pages/instance/Index.vue +++ b/theseus_gui/src/pages/instance/Index.vue @@ -147,13 +147,13 @@ import { import { process_listener, profile_listener } from '@/helpers/events' import { useRoute, useRouter } from 'vue-router' import { ref, onUnmounted } from 'vue' -import { convertFileSrc } from '@tauri-apps/api/tauri' import { handleError, useBreadcrumbs, useLoading } from '@/store/state' import { showInFolder } from '@/helpers/utils.js' import ContextMenu from '@/components/ui/ContextMenu.vue' import mixpanel from 'mixpanel-browser' import { PackageIcon } from '@/assets/icons/index.js' import ExportModal from '@/components/ui/ExportModal.vue' +import { convertFileSrc } from '@tauri-apps/api/tauri' const route = useRoute() @@ -278,6 +278,12 @@ const handleOptionsClick = async (args) => { const unlistenProfiles = await profile_listener(async (event) => { if (event.path === route.params.id) { + if (event.event === 'removed') { + await router.push({ + path: '/', + }) + return + } instance.value = await get(route.params.id).catch(handleError) } }) diff --git a/theseus_gui/src/pages/instance/Logs.vue b/theseus_gui/src/pages/instance/Logs.vue index 52dbedcb..101ebf8e 100644 --- a/theseus_gui/src/pages/instance/Logs.vue +++ b/theseus_gui/src/pages/instance/Logs.vue @@ -107,7 +107,7 @@ async function getLiveLog() { } async function getLogs() { - return (await get_logs(props.instance.uuid, true).catch(handleError)).reverse().map((log) => { + return (await get_logs(props.instance.path, true).catch(handleError)).reverse().map((log) => { log.name = dayjs( log.datetime_string.slice(0, 8) + 'T' + log.datetime_string.slice(9) ).calendar() @@ -149,7 +149,7 @@ watch(selectedLogIndex, async (newIndex) => { if (logs.value.length > 1 && newIndex !== 0) { logs.value[newIndex].stdout = 'Loading...' logs.value[newIndex].stdout = await get_output_by_datetime( - props.instance.uuid, + props.instance.path, logs.value[newIndex].datetime_string ).catch(handleError) } @@ -164,7 +164,7 @@ const deleteLog = async () => { let deleteIndex = selectedLogIndex.value selectedLogIndex.value = deleteIndex - 1 await delete_logs_by_datetime( - props.instance.uuid, + props.instance.path, logs.value[deleteIndex].datetime_string ).catch(handleError) await setLogs() diff --git a/theseus_gui/src/pages/instance/Mods.vue b/theseus_gui/src/pages/instance/Mods.vue index b9abba70..234d6dfd 100644 --- a/theseus_gui/src/pages/instance/Mods.vue +++ b/theseus_gui/src/pages/instance/Mods.vue @@ -331,7 +331,6 @@ import { CodeIcon, } from 'omorphia' import { computed, ref, watch } from 'vue' -import { convertFileSrc } from '@tauri-apps/api/tauri' import { useRouter } from 'vue-router' import { add_project_from_path, @@ -345,6 +344,7 @@ import { handleError } from '@/store/notifications.js' import mixpanel from 'mixpanel-browser' import { open } from '@tauri-apps/api/dialog' import { listen } from '@tauri-apps/api/event' +import { convertFileSrc } from '@tauri-apps/api/tauri' import { showInFolder } from '@/helpers/utils.js' import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage } from '@/assets/icons' diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index 56b3fac7..e15d03a2 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -98,7 +98,7 @@ async fn main() -> theseus::Result<()> { println!("running"); // Run a profile, running minecraft and store the RwLock to the process - let proc_lock = profile::run(&canonicalize(&profile_path)?).await?; + let proc_lock = profile::run(&profile_path).await?; let uuid = proc_lock.read().await.uuid; let pid = proc_lock.read().await.current_child.read().await.id();