diff --git a/theseus/src/api/logs.rs b/theseus/src/api/logs.rs index 2e3420ee..694ce0e8 100644 --- a/theseus/src/api/logs.rs +++ b/theseus/src/api/logs.rs @@ -31,20 +31,19 @@ impl Logs { #[tracing::instrument] pub async fn get_logs( - profile_uuid: uuid::Uuid, + profile_path: ProfilePathId, clear_contents: Option, ) -> crate::Result> { let state = State::get().await?; - 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 profile_path = + if let Some(p) = crate::profile::get(&profile_path, None).await? { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_path.to_string(), + ) + .into()); + }; let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; let mut logs = Vec::new(); @@ -77,19 +76,18 @@ pub async fn get_logs( #[tracing::instrument] pub async fn get_logs_by_datetime( - profile_uuid: uuid::Uuid, + profile_path: ProfilePathId, 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()); - }; + let profile_path = + if let Some(p) = crate::profile::get(&profile_path, None).await? { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_path.to_string(), + ) + .into()); + }; Ok(Logs { output: Some( get_output_by_datetime(&profile_path, &datetime_string).await?, @@ -111,17 +109,16 @@ pub async fn get_output_by_datetime( } #[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()); - }; +pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> { + let profile_path = + if let Some(p) = crate::profile::get(&profile_path, None).await? { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_path.to_string(), + ) + .into()); + }; let state = State::get().await?; let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; @@ -139,19 +136,18 @@ pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> { #[tracing::instrument] pub async fn delete_logs_by_datetime( - profile_uuid: uuid::Uuid, + profile_path: ProfilePathId, 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 profile_path = + if let Some(p) = crate::profile::get(&profile_path, None).await? { + p.profile_id() + } else { + return Err(crate::ErrorKind::UnmanagedProfileError( + profile_path.to_string(), + ) + .into()); + }; let state = State::get().await?; let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; diff --git a/theseus/src/api/mod.rs b/theseus/src/api/mod.rs index f58a538a..4bc94b5e 100644 --- a/theseus/src/api/mod.rs +++ b/theseus/src/api/mod.rs @@ -7,7 +7,6 @@ pub mod metadata; pub mod pack; pub mod process; pub mod profile; -pub mod profile_create; pub mod safety; pub mod settings; pub mod tags; @@ -26,8 +25,8 @@ pub mod prelude { data::*, event::CommandPayload, jre, metadata, pack, process, - profile::{self, Profile}, - profile_create, settings, + profile::{self, create, Profile}, + settings, state::JavaGlobals, state::{ProfilePathId, ProjectPathId}, util::{ diff --git a/theseus/src/api/pack/import/atlauncher.rs b/theseus/src/api/pack/import/atlauncher.rs index be250847..2edaa888 100644 --- a/theseus/src/api/pack/import/atlauncher.rs +++ b/theseus/src/api/pack/import/atlauncher.rs @@ -199,7 +199,7 @@ async fn import_atlauncher_unmanaged( let game_version = atinstance.id; let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile_create::get_loader_version_from_loader( + crate::profile::create::get_loader_version_from_loader( game_version.clone(), mod_loader, Some(atinstance.launcher.loader_version.version.clone()), diff --git a/theseus/src/api/pack/import/curseforge.rs b/theseus/src/api/pack/import/curseforge.rs index 27603e01..4387646b 100644 --- a/theseus/src/api/pack/import/curseforge.rs +++ b/theseus/src/api/pack/import/curseforge.rs @@ -105,7 +105,7 @@ pub async fn import_curseforge( let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla); let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile_create::get_loader_version_from_loader( + crate::profile::create::get_loader_version_from_loader( game_version.clone(), mod_loader, loader_version, diff --git a/theseus/src/api/pack/import/gdlauncher.rs b/theseus/src/api/pack/import/gdlauncher.rs index 6bf0f07c..6b642cb2 100644 --- a/theseus/src/api/pack/import/gdlauncher.rs +++ b/theseus/src/api/pack/import/gdlauncher.rs @@ -75,7 +75,7 @@ pub async fn import_gdlauncher( let loader_version = config.loader.loader_version; let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile_create::get_loader_version_from_loader( + crate::profile::create::get_loader_version_from_loader( game_version.clone(), mod_loader, loader_version, diff --git a/theseus/src/api/pack/import/mmc.rs b/theseus/src/api/pack/import/mmc.rs index 6cc87db6..d62cb6c9 100644 --- a/theseus/src/api/pack/import/mmc.rs +++ b/theseus/src/api/pack/import/mmc.rs @@ -38,8 +38,10 @@ pub struct MMCInstance { #[serde(deserialize_with = "deserialize_optional_bool")] pub managed_pack: Option, + #[serde(rename = "ManagedPackID")] pub managed_pack_id: Option, pub managed_pack_type: Option, + #[serde(rename = "ManagedPackVersionID")] pub managed_pack_version_id: Option, pub managed_pack_version_name: Option, diff --git a/theseus/src/api/pack/import/mod.rs b/theseus/src/api/pack/import/mod.rs index 0559f157..bc1e908c 100644 --- a/theseus/src/api/pack/import/mod.rs +++ b/theseus/src/api/pack/import/mod.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::{ prelude::ProfilePathId, + state::Profiles, util::{fetch, io}, }; @@ -112,6 +113,10 @@ pub async fn import_instance( .into()); } } + + // Check existing managed packs for potential updates + tokio::task::spawn(Profiles::update_modrinth_versions()); + tracing::debug!("Completed import."); Ok(()) } diff --git a/theseus/src/api/pack/install_from.rs b/theseus/src/api/pack/install_from.rs index bfb31d71..e382bacd 100644 --- a/theseus/src/api/pack/install_from.rs +++ b/theseus/src/api/pack/install_from.rs @@ -119,6 +119,7 @@ impl Default for CreatePackProfile { } } +#[derive(Clone)] pub struct CreatePack { pub file: bytes::Bytes, pub description: CreatePackDescription, @@ -337,7 +338,7 @@ pub async fn set_profile_information( let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla); let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile_create::get_loader_version_from_loader( + crate::profile::create::get_loader_version_from_loader( game_version.clone(), mod_loader, loader_version.cloned(), diff --git a/theseus/src/api/pack/install_mrpack.rs b/theseus/src/api/pack/install_mrpack.rs index 6ebb1e00..a905362e 100644 --- a/theseus/src/api/pack/install_mrpack.rs +++ b/theseus/src/api/pack/install_mrpack.rs @@ -6,8 +6,9 @@ use crate::pack::install_from::{ set_profile_information, EnvType, PackFile, PackFileHash, }; use crate::prelude::ProfilePathId; -use crate::state::SideType; +use crate::state::{ProfileInstallStage, Profiles, SideType}; use crate::util::fetch::{fetch_mirrors, write}; +use crate::util::io; use crate::State; use async_zip::tokio::read::seek::ZipFileReader; @@ -19,11 +20,14 @@ use super::install_from::{ CreatePackLocation, PackFormat, }; +/// Install a pack +/// Wrapper around install_pack_files that generates a pack creation description, and +/// attempts to install the pack files. If it fails, it will remove the profile (fail safely) /// Install a modpack from a mrpack file (a modrinth .zip format) #[theseus_macros::debug_pin] pub async fn install_zipped_mrpack( location: CreatePackLocation, - profile: ProfilePathId, + profile_path: ProfilePathId, ) -> crate::Result { // Get file from description let create_pack: CreatePack = match location { @@ -34,254 +38,355 @@ pub async fn install_zipped_mrpack( icon_url, } => { generate_pack_from_version_id( - project_id, version_id, title, icon_url, profile, + project_id, + version_id, + title, + icon_url, + profile_path.clone(), ) .await? } CreatePackLocation::FromFile { path } => { - generate_pack_from_file(path, profile).await? + generate_pack_from_file(path, profile_path.clone()).await? } }; + // Install pack files, and if it fails, fail safely by removing the profile + let result = install_zipped_mrpack_files(create_pack).await; + + // Check existing managed packs for potential updates + tokio::task::spawn(Profiles::update_modrinth_versions()); + + match result { + Ok(profile) => Ok(profile), + Err(err) => { + let _ = crate::api::profile::remove(&profile_path).await; + + Err(err) + } + } +} + +/// Install all pack files from a description +/// Does not remove the profile if it fails +#[theseus_macros::debug_pin] +pub async fn install_zipped_mrpack_files( + create_pack: CreatePack, +) -> crate::Result { + let state = &State::get().await?; + let file = create_pack.file; let description = create_pack.description.clone(); // make a copy for profile edit function let icon = create_pack.description.icon; let project_id = create_pack.description.project_id; let version_id = create_pack.description.version_id; let existing_loading_bar = create_pack.description.existing_loading_bar; - let profile = create_pack.description.profile_path; + let profile_path = create_pack.description.profile_path; - let state = &State::get().await?; + let reader: Cursor<&bytes::Bytes> = Cursor::new(&file); - let result = async { - let reader: Cursor<&bytes::Bytes> = Cursor::new(&file); + // Create zip reader around file + let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| { + crate::Error::from(crate::ErrorKind::InputError( + "Failed to read input modpack zip".to_string(), + )) + })?; - // Create zip reader around file - let mut zip_reader = - ZipFileReader::new(reader).await.map_err(|_| { - crate::Error::from(crate::ErrorKind::InputError( - "Failed to read input modpack zip".to_string(), - )) - })?; - - // Extract index of modrinth.index.json - let zip_index_option = zip_reader + // Extract index of modrinth.index.json + let zip_index_option = zip_reader + .file() + .entries() + .iter() + .position(|f| f.entry().filename() == "modrinth.index.json"); + if let Some(zip_index) = zip_index_option { + let mut manifest = String::new(); + let entry = zip_reader .file() .entries() - .iter() - .position(|f| f.entry().filename() == "modrinth.index.json"); - if let Some(zip_index) = zip_index_option { - let mut manifest = String::new(); - let entry = zip_reader - .file() - .entries() - .get(zip_index) - .unwrap() - .entry() - .clone(); - let mut reader = zip_reader.entry(zip_index).await?; - reader.read_to_string_checked(&mut manifest, &entry).await?; + .get(zip_index) + .unwrap() + .entry() + .clone(); + let mut reader = zip_reader.entry(zip_index).await?; + reader.read_to_string_checked(&mut manifest, &entry).await?; - let pack: PackFormat = serde_json::from_str(&manifest)?; + let pack: PackFormat = serde_json::from_str(&manifest)?; - if &*pack.game != "minecraft" { - return Err(crate::ErrorKind::InputError( - "Pack does not support Minecraft".to_string(), - ) - .into()); - } - - // Sets generated profile attributes to the pack ones (using profile::edit) - set_profile_information( - profile.clone(), - &description, - &pack.name, - &pack.dependencies, + if &*pack.game != "minecraft" { + return Err(crate::ErrorKind::InputError( + "Pack does not support Minecraft".to_string(), ) - .await?; + .into()); + } - let profile_full_path = profile.get_full_path().await?; - let profile = profile.clone(); - let result = async { - let loading_bar = init_or_edit_loading( - existing_loading_bar, - LoadingBarType::PackDownload { - profile_path: profile_full_path.clone(), - pack_name: pack.name.clone(), - icon, - pack_id: project_id, - pack_version: version_id, - }, - 100.0, - "Downloading modpack", - ) - .await?; + // Sets generated profile attributes to the pack ones (using profile::edit) + set_profile_information( + profile_path.clone(), + &description, + &pack.name, + &pack.dependencies, + ) + .await?; - let num_files = pack.files.len(); - use futures::StreamExt; - loading_try_for_each_concurrent( - futures::stream::iter(pack.files.into_iter()) - .map(Ok::), - None, - Some(&loading_bar), - 70.0, - num_files, - None, - |project| { - let profile_full_path = profile_full_path.clone(); - async move { - //TODO: Future update: prompt user for optional files in a modpack - if let Some(env) = project.env { - if env - .get(&EnvType::Client) - .map(|x| x == &SideType::Unsupported) - .unwrap_or(false) - { - return Ok(()); - } - } + let profile_path = profile_path.clone(); + let loading_bar = init_or_edit_loading( + existing_loading_bar, + LoadingBarType::PackDownload { + profile_path: profile_path.get_full_path().await?.clone(), + pack_name: pack.name.clone(), + icon, + pack_id: project_id, + pack_version: version_id, + }, + 100.0, + "Downloading modpack", + ) + .await?; - let file = fetch_mirrors( - &project - .downloads - .iter() - .map(|x| &**x) - .collect::>(), - project - .hashes - .get(&PackFileHash::Sha1) - .map(|x| &**x), - &state.fetch_semaphore, - ) - .await?; - - let path = std::path::Path::new(&project.path) - .components() - .next(); - if let Some(path) = path { - match path { - Component::CurDir - | Component::Normal(_) => { - let path = profile_full_path - .join(project.path); - write( - &path, - &file, - &state.io_semaphore, - ) - .await?; - } - _ => {} - }; - } - Ok(()) + let num_files = pack.files.len(); + use futures::StreamExt; + loading_try_for_each_concurrent( + futures::stream::iter(pack.files.into_iter()) + .map(Ok::), + None, + Some(&loading_bar), + 70.0, + num_files, + None, + |project| { + 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 { + if env + .get(&EnvType::Client) + .map(|x| x == &SideType::Unsupported) + .unwrap_or(false) + { + return Ok(()); } - }, - ) - .await?; - - emit_loading(&loading_bar, 0.0, Some("Extracting overrides")) - .await?; - - let mut total_len = 0; - - for index in 0..zip_reader.file().entries().len() { - let file = - zip_reader.file().entries().get(index).unwrap().entry(); - - if (file.filename().starts_with("overrides") - || file.filename().starts_with("client_overrides")) - && !file.filename().ends_with('/') - { - total_len += 1; } - } - for index in 0..zip_reader.file().entries().len() { - let file = zip_reader - .file() - .entries() - .get(index) - .unwrap() - .entry() - .clone(); - - let file_path = PathBuf::from(file.filename()); - if (file.filename().starts_with("overrides") - || file.filename().starts_with("client_overrides")) - && !file.filename().ends_with('/') - { - // Reads the file into the 'content' variable - let mut content = Vec::new(); - let mut reader = zip_reader.entry(index).await?; - reader.read_to_end_checked(&mut content, &file).await?; - - let mut new_path = PathBuf::new(); - let components = file_path.components().skip(1); - - for component in components { - new_path.push(component); - } - - if new_path.file_name().is_some() { - write( - &profile_full_path.join(new_path), - &content, - &state.io_semaphore, - ) - .await?; - } - - emit_loading( - &loading_bar, - 30.0 / total_len as f64, - Some(&format!( - "Extracting override {}/{}", - index, total_len - )), - ) - .await?; - } - } - - if let Some(profile_val) = - crate::api::profile::get(&profile, None).await? - { - crate::launcher::install_minecraft( - &profile_val, - Some(loading_bar), + let file = fetch_mirrors( + &project + .downloads + .iter() + .map(|x| &**x) + .collect::>(), + project.hashes.get(&PackFileHash::Sha1).map(|x| &**x), + &state.fetch_semaphore, ) .await?; - State::sync().await?; + let path = + std::path::Path::new(&project.path).components().next(); + if let Some(path) = path { + match path { + Component::CurDir | Component::Normal(_) => { + let path = profile_path + .get_full_path() + .await? + .join(project.path); + write(&path, &file, &state.io_semaphore) + .await?; + } + _ => {} + }; + } + Ok(()) + } + }, + ) + .await?; + + emit_loading(&loading_bar, 0.0, Some("Extracting overrides")).await?; + + let mut total_len = 0; + + for index in 0..zip_reader.file().entries().len() { + let file = zip_reader.file().entries().get(index).unwrap().entry(); + + if (file.filename().starts_with("overrides") + || file.filename().starts_with("client_overrides")) + && !file.filename().ends_with('/') + { + total_len += 1; + } + } + + for index in 0..zip_reader.file().entries().len() { + let file = zip_reader + .file() + .entries() + .get(index) + .unwrap() + .entry() + .clone(); + + let file_path = PathBuf::from(file.filename()); + if (file.filename().starts_with("overrides") + || file.filename().starts_with("client_overrides")) + && !file.filename().ends_with('/') + { + // Reads the file into the 'content' variable + let mut content = Vec::new(); + let mut reader = zip_reader.entry(index).await?; + reader.read_to_end_checked(&mut content, &file).await?; + + let mut new_path = PathBuf::new(); + let components = file_path.components().skip(1); + + for component in components { + new_path.push(component); } - Ok::(profile.clone()) - } - .await; - - match result { - Ok(profile) => Ok(profile), - Err(err) => { - let _ = crate::api::profile::remove(&profile).await; - - Err(err) + if new_path.file_name().is_some() { + write( + &profile_path.get_full_path().await?.join(new_path), + &content, + &state.io_semaphore, + ) + .await?; } + + emit_loading( + &loading_bar, + 30.0 / total_len as f64, + Some(&format!( + "Extracting override {}/{}", + index, total_len + )), + ) + .await?; } - } else { - Err(crate::Error::from(crate::ErrorKind::InputError( - "No pack manifest found in mrpack".to_string(), - ))) } - } - .await; - match result { - Ok(profile) => Ok(profile), - Err(err) => { - let _ = crate::api::profile::remove(&profile).await; + if let Some(profile_val) = + crate::api::profile::get(&profile_path, None).await? + { + crate::launcher::install_minecraft(&profile_val, Some(loading_bar)) + .await?; - Err(err) + State::sync().await?; } + + Ok::(profile_path.clone()) + } else { + Err(crate::Error::from(crate::ErrorKind::InputError( + "No pack manifest found in mrpack".to_string(), + ))) + } +} + +#[tracing::instrument(skip(mrpack_file))] +#[theseus_macros::debug_pin] +pub async fn remove_all_related_files( + profile_path: ProfilePathId, + mrpack_file: bytes::Bytes, +) -> crate::Result<()> { + let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file); + + // Create zip reader around file + let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| { + crate::Error::from(crate::ErrorKind::InputError( + "Failed to read input modpack zip".to_string(), + )) + })?; + + // Extract index of modrinth.index.json + let zip_index_option = zip_reader + .file() + .entries() + .iter() + .position(|f| f.entry().filename() == "modrinth.index.json"); + if let Some(zip_index) = zip_index_option { + let mut manifest = String::new(); + let entry = zip_reader + .file() + .entries() + .get(zip_index) + .unwrap() + .entry() + .clone(); + let mut reader = zip_reader.entry(zip_index).await?; + reader.read_to_string_checked(&mut manifest, &entry).await?; + + let pack: PackFormat = serde_json::from_str(&manifest)?; + + if &*pack.game != "minecraft" { + return Err(crate::ErrorKind::InputError( + "Pack does not support Minecraft".to_string(), + ) + .into()); + } + + // Set install stage to installing, and do not change it back (as files are being removed and are not being reinstalled here) + crate::api::profile::edit(&profile_path, |prof| { + prof.install_stage = ProfileInstallStage::PackInstalling; + async { Ok(()) } + }) + .await?; + + let num_files = pack.files.len(); + use futures::StreamExt; + loading_try_for_each_concurrent( + futures::stream::iter(pack.files.into_iter()) + .map(Ok::), + None, + None, + 0.0, + num_files, + None, + |project| { + let profile_path = profile_path.clone(); + async move { + // Remove this file if a corresponding one exists in the filesystem + let existing_file = + profile_path.get_full_path().await?.join(&project.path); + if existing_file.exists() { + io::remove_file(&existing_file).await?; + } + + Ok(()) + } + }, + ) + .await?; + + // Iterate over each 'overrides' file and remove it + for index in 0..zip_reader.file().entries().len() { + let file = zip_reader + .file() + .entries() + .get(index) + .unwrap() + .entry() + .clone(); + + let file_path = PathBuf::from(file.filename()); + if (file.filename().starts_with("overrides") + || file.filename().starts_with("client_overrides")) + && !file.filename().ends_with('/') + { + let mut new_path = PathBuf::new(); + let components = file_path.components().skip(1); + + for component in components { + new_path.push(component); + } + + // Remove this file if a corresponding one exists in the filesystem + let existing_file = + profile_path.get_full_path().await?.join(&new_path); + if existing_file.exists() { + io::remove_file(&existing_file).await?; + } + } + } + Ok(()) + } else { + Err(crate::Error::from(crate::ErrorKind::InputError( + "No pack manifest found in mrpack".to_string(), + ))) } } diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile/create.rs similarity index 100% rename from theseus/src/api/profile_create.rs rename to theseus/src/api/profile/create.rs diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile/mod.rs similarity index 97% rename from theseus/src/api/profile.rs rename to theseus/src/api/profile/mod.rs index fd11085f..41f1c97e 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile/mod.rs @@ -1,4 +1,5 @@ //! Theseus profile management interface + use crate::event::emit::{ emit_loading, init_loading, loading_try_for_each_concurrent, }; @@ -6,8 +7,8 @@ use crate::event::LoadingBarType; use crate::pack::install_from::{ EnvType, PackDependency, PackFile, PackFileHash, PackFormat, }; -use crate::prelude::JavaVersion; -use crate::state::{ProfilePathId, ProjectMetadata, ProjectPathId}; +use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; +use crate::state::ProjectMetadata; use crate::util::io::{self, IOError}; use crate::{ @@ -21,7 +22,9 @@ pub use crate::{ }; use async_zip::tokio::write::ZipFileWriter; use async_zip::{Compression, ZipEntryBuilder}; + use std::collections::HashMap; + use std::{ future::Future, path::{Path, PathBuf}, @@ -30,6 +33,9 @@ use std::{ use tokio::io::AsyncReadExt; use tokio::{fs::File, process::Command, sync::RwLock}; +pub mod create; +pub mod update; + /// Remove a profile #[tracing::instrument] pub async fn remove(path: &ProfilePathId) -> crate::Result<()> { @@ -56,7 +62,6 @@ pub async fn get( clear_projects: Option, ) -> crate::Result> { let state = State::get().await?; - let profiles = state.profiles.read().await; let mut profile = profiles.0.get(path).cloned(); @@ -253,7 +258,7 @@ pub async fn install(path: &ProfilePathId) -> crate::Result<()> { #[tracing::instrument] #[theseus_macros::debug_pin] -pub async fn update_all( +pub async fn update_all_projects( profile_path: &ProfilePathId, ) -> crate::Result> { if let Some(profile) = get(profile_path, None).await? { @@ -519,6 +524,23 @@ pub async fn remove_project( } } +/// Gets whether project is a managed modrinth pack +#[tracing::instrument] +pub async fn is_managed_modrinth_pack( + profile: &ProfilePathId, +) -> crate::Result { + if let Some(profile) = get(profile, None).await? { + if let Some(linked_data) = profile.metadata.linked_data { + return Ok(linked_data.project_id.is_some() + && linked_data.version_id.is_some()); + } + Ok(false) + } else { + Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string()) + .as_error()) + } +} + /// Exports the profile to a Modrinth-formatted .mrpack file // Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) #[tracing::instrument(skip_all)] diff --git a/theseus/src/api/profile/update.rs b/theseus/src/api/profile/update.rs new file mode 100644 index 00000000..952d5587 --- /dev/null +++ b/theseus/src/api/profile/update.rs @@ -0,0 +1,203 @@ +use crate::{ + event::{ + emit::{emit_profile, loading_try_for_each_concurrent}, + ProfilePayloadType, + }, + pack::{self, install_from::generate_pack_from_version_id}, + prelude::{ProfilePathId, ProjectPathId}, + profile::get, + state::Project, + State, +}; +use futures::try_join; + +/// Updates a managed modrinth pack to the cached latest version found in 'modrinth_update_version' +#[tracing::instrument] +#[theseus_macros::debug_pin] +pub async fn update_managed_modrinth( + profile_path: &ProfilePathId, +) -> crate::Result<()> { + let profile = get(profile_path, None).await?.ok_or_else(|| { + crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) + .as_error() + })?; + + let unmanaged_err = || { + crate::ErrorKind::InputError( + format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path), + ) + }; + + // Extract modrinth pack information, if appropriate + let linked_data = profile + .metadata + .linked_data + .as_ref() + .ok_or_else(unmanaged_err)?; + let project_id: &String = + linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?; + let version_id = + linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?; + + // extract modrinth_update_version, returning Ok(()) if it is none + let modrinth_update_version = match profile.modrinth_update_version { + Some(ref x) if x != version_id => x, + _ => return Ok(()), // No update version, or no update needed, return Ok(()) + }; + + // Replace the pack with the new version + replace_managed_modrinth( + profile_path, + &profile, + project_id, + version_id, + Some(modrinth_update_version), + ) + .await?; + + emit_profile( + profile.uuid, + profile.path, + &profile.metadata.name, + ProfilePayloadType::Edited, + ) + .await?; + + State::sync().await?; + Ok(()) +} + +/// Repair a managed modrinth pack by 'updating' it to the current version +#[tracing::instrument] +#[theseus_macros::debug_pin] +pub async fn repair_managed_modrinth( + profile_path: &ProfilePathId, +) -> crate::Result<()> { + let profile = get(profile_path, None).await?.ok_or_else(|| { + crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) + .as_error() + })?; + + let unmanaged_err = || { + crate::ErrorKind::InputError( + format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path), + ) + }; + + // For repairing specifically, first we remove all installed projects (to ensure we do remove ones that aren't in the pack) + // We do a project removal followed by removing everything in the .mrpack, to ensure we only + // remove relevant projects and not things like save files + let projects_map = profile.projects.clone(); + let stream = futures::stream::iter( + projects_map + .into_iter() + .map(Ok::<(ProjectPathId, Project), crate::Error>), + ); + loading_try_for_each_concurrent( + stream, + None, + None, + 0.0, + 0, + None, + |(project_id, _)| { + let profile = profile.clone(); + async move { + profile.remove_project(&project_id, Some(true)).await?; + Ok(()) + } + }, + ) + .await?; + + // Extract modrinth pack information, if appropriate + let linked_data = profile + .metadata + .linked_data + .as_ref() + .ok_or_else(unmanaged_err)?; + let project_id: &String = + linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?; + let version_id = + linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?; + + // Replace the pack with the same version + replace_managed_modrinth( + profile_path, + &profile, + project_id, + version_id, + None, + ) + .await?; + + emit_profile( + profile.uuid, + profile.path, + &profile.metadata.name, + ProfilePayloadType::Edited, + ) + .await?; + + State::sync().await?; + Ok(()) +} + +/// Replace a managed modrinth pack with a new version +/// If new_version_id is None, the pack is 'reinstalled' in-place +#[tracing::instrument(skip(profile))] +#[theseus_macros::debug_pin] +async fn replace_managed_modrinth( + profile_path: &ProfilePathId, + profile: &crate::state::Profile, + project_id: &String, + version_id: &String, + new_version_id: Option<&String>, +) -> crate::Result<()> { + // Fetch .mrpacks for both old and new versions + // TODO: this will need to be updated if we revert the hacky pack method we needed for compiler speed + let old_pack_creator = generate_pack_from_version_id( + project_id.clone(), + version_id.clone(), + profile.metadata.name.clone(), + None, + profile_path.clone(), + ); + + // download in parallel, then join. If new_version_id is None, we don't need to download the new pack, so we clone the old one + let (old_pack_creator, new_pack_creator) = + if let Some(new_version_id) = new_version_id { + try_join!( + old_pack_creator, + generate_pack_from_version_id( + project_id.clone(), + new_version_id.clone(), + profile.metadata.name.clone(), + None, + profile_path.clone() + ) + )? + } else { + let mut old_pack_creator = old_pack_creator.await?; + old_pack_creator.description.existing_loading_bar = None; + (old_pack_creator.clone(), old_pack_creator) + }; + + // Removal - remove all files that were added by the old pack + // - remove all installed projects + // - remove all overrides + pack::install_mrpack::remove_all_related_files( + profile_path.clone(), + old_pack_creator.file, + ) + .await?; + + // Reinstallation - install all files that are added by the new pack + // - install all projects + // - install all overrides + // - edits the profile to update the new data + // - (functionals almost identically to rteinstalling the pack 'in-place') + pack::install_mrpack::install_zipped_mrpack_files(new_pack_creator).await?; + + Ok(()) +} diff --git a/theseus/src/error.rs b/theseus/src/error.rs index ea44b4ce..0caee272 100644 --- a/theseus/src/error.rs +++ b/theseus/src/error.rs @@ -1,5 +1,5 @@ //! Theseus error type -use crate::{profile_create, util}; +use crate::{profile, util}; use tracing_error::InstrumentError; #[derive(thiserror::Error, Debug)] @@ -68,7 +68,7 @@ pub enum ErrorKind { UnmanagedProfileError(String), #[error("Could not create profile: {0}")] - ProfileCreationError(#[from] profile_create::ProfileCreationError), + ProfileCreationError(#[from] profile::create::ProfileCreationError), #[error("User is not logged in, no credentials available!")] NoCredentialsError, diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 5334194a..c16a1d9d 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -185,6 +185,7 @@ impl State { tokio::task::spawn(Metadata::update()); tokio::task::spawn(Tags::update()); tokio::task::spawn(Profiles::update_projects()); + tokio::task::spawn(Profiles::update_modrinth_versions()); tokio::task::spawn(Settings::update_java()); } diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index b36a67f4..bb1b1434 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -51,6 +51,7 @@ pub enum ProfileInstallStage { #[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 { @@ -151,6 +152,8 @@ pub struct Profile { #[serde(skip_serializing_if = "Option::is_none")] pub hooks: Option, pub projects: HashMap, + #[serde(default)] + pub modrinth_update_version: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -266,6 +269,7 @@ impl Profile { memory: None, resolution: None, hooks: None, + modrinth_update_version: None, }) } @@ -386,13 +390,12 @@ impl Profile { let mut read_paths = |path: &str| { 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) - .map_err(|e| IOError::with_path(e, &path))? + for subpath in std::fs::read_dir(&new_path) + .map_err(|e| IOError::with_path(e, &new_path))? { - let path = path.map_err(IOError::from)?.path(); - if path.is_file() { - files.push(path); + let subpath = subpath.map_err(IOError::from)?.path(); + if subpath.is_file() { + files.push(subpath); } } } @@ -770,6 +773,86 @@ impl Profiles { }; } + #[tracing::instrument] + #[theseus_macros::debug_pin] + pub async fn update_modrinth_versions() { + let res = async { + let state = State::get().await?; + // Temporarily store all profiles that have modrinth linked data + let mut modrinth_updatables: Vec<(ProfilePathId, String)> = + Vec::new(); + { + let profiles = state.profiles.read().await; + for (profile_path, profile) in profiles.0.iter() { + if let Some(linked_data) = &profile.metadata.linked_data { + if let Some(linked_project) = &linked_data.project_id { + modrinth_updatables.push(( + profile_path.clone(), + linked_project.clone(), + )); + } + } + } + } + + // Fetch online from Modrinth each latest version + future::try_join_all(modrinth_updatables.into_iter().map( + |(profile_path, linked_project)| { + let profile_path = profile_path; + let linked_project = linked_project; + let state = state.clone(); + async move { + let versions: Vec = fetch_json( + Method::GET, + &format!( + "{}project/{}/version", + MODRINTH_API_URL, + linked_project.clone() + ), + None, + None, + &state.fetch_semaphore, + ) + .await?; + + // Versions are pre-sorted in labrinth (by versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));) + // so we can just take the first one + let mut new_profiles = state.profiles.write().await; + if let Some(profile) = + new_profiles.0.get_mut(&profile_path) + { + if let Some(recent_version) = versions.get(0) { + profile.modrinth_update_version = + Some(recent_version.id.clone()); + } else { + profile.modrinth_update_version = None; + } + } + drop(new_profiles); + + Ok::<(), crate::Error>(()) + } + }, + )) + .await?; + + { + let profiles = state.profiles.read().await; + profiles.sync().await?; + } + + Ok::<(), crate::Error>(()) + } + .await; + + match res { + Ok(()) => {} + Err(err) => { + tracing::warn!("Unable to update modrinth versions: {err}") + } + }; + } + #[tracing::instrument(skip(self, profile))] #[theseus_macros::debug_pin] pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> { diff --git a/theseus_cli/src/subcommands/profile.rs b/theseus_cli/src/subcommands/profile.rs index a2c773f9..97a42287 100644 --- a/theseus_cli/src/subcommands/profile.rs +++ b/theseus_cli/src/subcommands/profile.rs @@ -10,7 +10,7 @@ use paris::*; use std::path::{Path, PathBuf}; use tabled::Tabled; use theseus::prelude::*; -use theseus::profile_create::profile_create; +use theseus::profile::create::profile_create; use tokio::fs; use tokio_stream::wrappers::ReadDirStream; diff --git a/theseus_gui/src-tauri/src/api/logs.rs b/theseus_gui/src-tauri/src/api/logs.rs index 1cb40f7d..7e253ab9 100644 --- a/theseus_gui/src-tauri/src/api/logs.rs +++ b/theseus_gui/src-tauri/src/api/logs.rs @@ -1,6 +1,8 @@ use crate::api::Result; -use theseus::logs::{self, Logs}; -use uuid::Uuid; +use theseus::{ + logs::{self, Logs}, + prelude::ProfilePathId, +}; /* A log is a struct containing the datetime string, stdout, and stderr, as follows: @@ -27,10 +29,10 @@ pub fn init() -> tauri::plugin::TauriPlugin { /// Get all Logs for a profile, sorted by datetime #[tauri::command] pub async fn logs_get_logs( - profile_uuid: Uuid, + profile_path: ProfilePathId, clear_contents: Option, ) -> Result> { - let val = logs::get_logs(profile_uuid, clear_contents).await?; + let val = logs::get_logs(profile_path, clear_contents).await?; Ok(val) } @@ -38,25 +40,25 @@ pub async fn logs_get_logs( /// Get a Log struct for a profile by profile id and datetime string #[tauri::command] pub async fn logs_get_logs_by_datetime( - profile_uuid: Uuid, + profile_path: ProfilePathId, datetime_string: String, ) -> Result { - Ok(logs::get_logs_by_datetime(profile_uuid, datetime_string).await?) + Ok(logs::get_logs_by_datetime(profile_path, datetime_string).await?) } /// Get the stdout for a profile by profile id and datetime string #[tauri::command] pub async fn logs_get_output_by_datetime( - profile_uuid: Uuid, + profile_path: ProfilePathId, datetime_string: String, ) -> Result { let profile_path = if let Some(p) = - crate::profile::get_by_uuid(profile_uuid, None).await? + crate::profile::get(&profile_path, None).await? { p.profile_id() } else { return Err(theseus::Error::from( - theseus::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()), + theseus::ErrorKind::UnmanagedProfileError(profile_path.to_string()), ) .into()); }; @@ -66,15 +68,15 @@ pub async fn logs_get_output_by_datetime( /// Delete all logs for a profile by profile id #[tauri::command] -pub async fn logs_delete_logs(profile_uuid: Uuid) -> Result<()> { - Ok(logs::delete_logs(profile_uuid).await?) +pub async fn logs_delete_logs(profile_path: ProfilePathId) -> Result<()> { + Ok(logs::delete_logs(profile_path).await?) } /// Delete a log for a profile by profile id and datetime string #[tauri::command] pub async fn logs_delete_logs_by_datetime( - profile_uuid: Uuid, + profile_path: ProfilePathId, datetime_string: String, ) -> Result<()> { - Ok(logs::delete_logs_by_datetime(profile_uuid, &datetime_string).await?) + Ok(logs::delete_logs_by_datetime(profile_path, &datetime_string).await?) } diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index a2642ae6..d83a8881 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -21,6 +21,9 @@ pub fn init() -> tauri::plugin::TauriPlugin { profile_add_project_from_path, profile_toggle_disable_project, profile_remove_project, + profile_update_managed_modrinth, + profile_repair_managed_modrinth, + profile_is_managed_modrinth, profile_run, profile_run_wait, profile_run_credentials, @@ -105,7 +108,7 @@ pub async fn profile_install(path: ProfilePathId) -> Result<()> { pub async fn profile_update_all( path: ProfilePathId, ) -> Result> { - Ok(profile::update_all(&path).await?) + Ok(profile::update_all_projects(&path).await?) } /// Updates a specified project @@ -162,6 +165,28 @@ pub async fn profile_remove_project( Ok(()) } +// Updates a managed Modrinth profile +#[tauri::command] +pub async fn profile_update_managed_modrinth( + path: ProfilePathId, +) -> Result<()> { + Ok(profile::update::update_managed_modrinth(&path).await?) +} + +// Repairs a managed Modrinth profile by updating it to the current version +#[tauri::command] +pub async fn profile_repair_managed_modrinth( + path: ProfilePathId, +) -> Result<()> { + Ok(profile::update::repair_managed_modrinth(&path).await?) +} + +// Gets if a profile is managed by Modrinth +#[tauri::command] +pub async fn profile_is_managed_modrinth(path: ProfilePathId) -> Result { + Ok(profile::is_managed_modrinth_pack(&path).await?) +} + // Exports a profile to a .mrpack file (export_location should end in .mrpack) // invoke('profile_export_mrpack') #[tauri::command] diff --git a/theseus_gui/src-tauri/src/api/profile_create.rs b/theseus_gui/src-tauri/src/api/profile_create.rs index 03e363e5..2d9b1ad1 100644 --- a/theseus_gui/src-tauri/src/api/profile_create.rs +++ b/theseus_gui/src-tauri/src/api/profile_create.rs @@ -18,7 +18,7 @@ pub async fn profile_create( 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 { - let res = profile_create::profile_create( + let res = profile::create::profile_create( name, game_version, modloader, diff --git a/theseus_gui/src/helpers/logs.js b/theseus_gui/src/helpers/logs.js index d1782945..6316ca99 100644 --- a/theseus_gui/src/helpers/logs.js +++ b/theseus_gui/src/helpers/logs.js @@ -17,26 +17,26 @@ pub struct Logs { /// Get all logs that exist for a given profile /// This is returned as an array of Log objects, sorted by datetime_string (the folder name, when the log was created) -export async function get_logs(profileUuid, clearContents) { - return await invoke('plugin:logs|logs_get_logs', { profileUuid, clearContents }) +export async function get_logs(profilePath, clearContents) { + return await invoke('plugin:logs|logs_get_logs', { profilePath, clearContents }) } /// Get a profile's log by datetime_string (the folder name, when the log was created) -export async function get_logs_by_datetime(profileUuid, datetimeString) { - return await invoke('plugin:logs|logs_get_logs_by_datetime', { profileUuid, datetimeString }) +export async function get_logs_by_datetime(profilePath, datetimeString) { + return await invoke('plugin:logs|logs_get_logs_by_datetime', { profilePath, datetimeString }) } /// Get a profile's stdout only by datetime_string (the folder name, when the log was created) -export async function get_output_by_datetime(profileUuid, datetimeString) { - return await invoke('plugin:logs|logs_get_output_by_datetime', { profileUuid, datetimeString }) +export async function get_output_by_datetime(profilePath, datetimeString) { + return await invoke('plugin:logs|logs_get_output_by_datetime', { profilePath, datetimeString }) } /// Delete a profile's log by datetime_string (the folder name, when the log was created) -export async function delete_logs_by_datetime(profileUuid, datetimeString) { - return await invoke('plugin:logs|logs_delete_logs_by_datetime', { profileUuid, datetimeString }) +export async function delete_logs_by_datetime(profilePath, datetimeString) { + return await invoke('plugin:logs|logs_delete_logs_by_datetime', { profilePath, datetimeString }) } /// Delete all logs for a given profile -export async function delete_logs(profileUuid) { - return await invoke('plugin:logs|logs_delete_logs', { profileUuid }) +export async function delete_logs(profilePath) { + return await invoke('plugin:logs|logs_delete_logs', { profilePath }) } diff --git a/theseus_gui/src/helpers/profile.js b/theseus_gui/src/helpers/profile.js index b6eb21b3..59c52b81 100644 --- a/theseus_gui/src/helpers/profile.js +++ b/theseus_gui/src/helpers/profile.js @@ -94,6 +94,21 @@ export async function remove_project(path, projectPath) { return await invoke('plugin:profile|profile_remove_project', { path, projectPath }) } +// Update a managed Modrinth profile +export async function update_managed_modrinth(path) { + return await invoke('plugin:profile|profile_update_managed_modrinth', { path }) +} + +// Repair a managed Modrinth profile +export async function update_repair_modrinth(path) { + return await invoke('plugin:profile|profile_repair_managed_modrinth', { path }) +} + +// Gets whether a profile is managed by Modrinth +export async function is_managed_modrinth(path) { + return await invoke('plugin:profile|profile_is_managed_modrinth', { path }) +} + // Export a profile to .mrpack /// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs') // Version id is optional (ie: 1.1.5) diff --git a/theseus_gui/src/main.js b/theseus_gui/src/main.js index fcdc1ac6..a799ee14 100644 --- a/theseus_gui/src/main.js +++ b/theseus_gui/src/main.js @@ -9,6 +9,8 @@ import FloatingVue from 'floating-vue' import { get_opening_command, initialize_state } from '@/helpers/state' import loadCssMixin from './mixins/macCssFix.js' import { get } from '@/helpers/settings' +import { invoke } from '@tauri-apps/api' +import { isDev } from './helpers/utils.js' const pinia = createPinia() @@ -20,6 +22,19 @@ app.mixin(loadCssMixin) const mountedApp = app.mount('#app') +const raw_invoke = async (plugin, fn, args) => { + return await invoke('plugin:' + plugin + '|' + fn, args) +} +isDev() + .then((dev) => { + if (dev) { + window.raw_invoke = raw_invoke + } + }) + .catch((err) => { + console.error(err) + }) + initialize_state() .then(() => { // First, redirect to other landing page if we have that setting diff --git a/theseus_gui/src/pages/instance/Options.vue b/theseus_gui/src/pages/instance/Options.vue index b74c11ad..59f80124 100644 --- a/theseus_gui/src/pages/instance/Options.vue +++ b/theseus_gui/src/pages/instance/Options.vue @@ -277,8 +277,8 @@ +
+ + +
+