diff --git a/theseus/src/api/pack/install_from.rs b/theseus/src/api/pack/install_from.rs index 44a28515..0c8e94cd 100644 --- a/theseus/src/api/pack/install_from.rs +++ b/theseus/src/api/pack/install_from.rs @@ -10,7 +10,7 @@ use crate::util::fetch::{ fetch, fetch_advanced, fetch_json, write_cached_icon, }; use crate::util::io; -use crate::State; +use crate::{InnerProjectPathUnix, State}; use reqwest::Method; use serde::{Deserialize, Serialize}; @@ -33,7 +33,7 @@ pub struct PackFormat { #[derive(Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PackFile { - pub path: String, + pub path: InnerProjectPathUnix, pub hashes: HashMap, pub env: Option>, pub downloads: Vec, diff --git a/theseus/src/api/pack/install_mrpack.rs b/theseus/src/api/pack/install_mrpack.rs index b28f4160..ee15ae03 100644 --- a/theseus/src/api/pack/install_mrpack.rs +++ b/theseus/src/api/pack/install_mrpack.rs @@ -189,10 +189,7 @@ pub async fn install_zipped_mrpack_files( .await?; drop(creds); - // Convert windows path to unix path. - // .mrpacks no longer generate windows paths, but this is here for backwards compatibility before this was fixed - // https://github.com/modrinth/theseus/issues/595 - let project_path = project.path.replace('\\', "/"); + let project_path = project.path.to_string(); let path = std::path::Path::new(&project_path).components().next(); @@ -403,7 +400,10 @@ pub async fn remove_all_related_files( // Iterate over all Modrinth project file paths in the json, and remove them // (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized) for file in pack.files { - let path = profile_path.get_full_path().await?.join(file.path); + let path: PathBuf = profile_path + .get_full_path() + .await? + .join(file.path.to_string()); if path.exists() { io::remove_file(&path).await?; } diff --git a/theseus/src/api/profile/mod.rs b/theseus/src/api/profile/mod.rs index e410f21e..52e006eb 100644 --- a/theseus/src/api/profile/mod.rs +++ b/theseus/src/api/profile/mod.rs @@ -8,7 +8,7 @@ use crate::pack::install_from::{ EnvType, PackDependency, PackFile, PackFileHash, PackFormat, }; use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; -use crate::state::{ProjectMetadata, SideType}; +use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType}; use crate::util::fetch; use crate::util::io::{self, IOError}; @@ -25,8 +25,9 @@ use async_zip::tokio::write::ZipFileWriter; use async_zip::{Compression, ZipEntryBuilder}; use serde_json::json; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::iter::FromIterator; use std::{ future::Future, path::{Path, PathBuf}, @@ -570,7 +571,7 @@ pub async fn remove_project( pub async fn export_mrpack( profile_path: &ProfilePathId, export_path: PathBuf, - included_overrides: Vec, // which folders to include in the overrides + included_export_candidates: Vec, // which folders/files to include in the export version_id: Option, description: Option, _name: Option, @@ -585,8 +586,8 @@ pub async fn export_mrpack( )) })?; - // remove .DS_Store files from included_overrides - let included_overrides = included_overrides + // remove .DS_Store files from included_export_candidates + let included_export_candidates = included_export_candidates .into_iter() .filter(|x| { if let Some(f) = PathBuf::from(x).file_name() { @@ -607,13 +608,17 @@ pub async fn export_mrpack( // Create mrpack json configuration file let version_id = version_id.unwrap_or("1.0.0".to_string()); - let packfile = + let mut packfile = create_mrpack_json(&profile, version_id, description).await?; - let modrinth_path_list = get_modrinth_pack_list(&packfile); + let included_candidates_set = + HashSet::<_>::from_iter(included_export_candidates.iter()); + packfile.files.retain(|f| { + included_candidates_set.contains(&f.path.get_topmost_two_components()) + }); // Build vec of all files in the folder let mut path_list = Vec::new(); - build_folder(profile_base_path, &mut path_list).await?; + add_all_recursive_folder_paths(profile_base_path, &mut path_list).await?; // Initialize loading bar let loading_bar = init_loading( @@ -631,38 +636,13 @@ pub async fn export_mrpack( for path in path_list { emit_loading(&loading_bar, 1.0, None).await?; - // Get local path of file, relative to profile folder - let relative_path = path.strip_prefix(profile_base_path)?; - - // Get highest level folder pair ('a/b' in 'a/b/c', 'a' in 'a') - // We only go one layer deep for the sake of not having a huge list of overrides - let topmost_two = relative_path.iter().take(2).collect::>(); - - // a,b => a/b - // a => a - let topmost = match topmost_two.len() { - 2 => PathBuf::from(topmost_two[0]).join(topmost_two[1]), - 1 => PathBuf::from(topmost_two[0]), - _ => { - return Err(crate::ErrorKind::OtherError( - "No topmost folder found".to_string(), - ) - .into()) - } - } - .to_string_lossy() - .to_string(); - - if !included_overrides.contains(&topmost) { - continue; - } - - let relative_path: std::borrow::Cow = - relative_path.to_string_lossy(); - let relative_path = relative_path.replace('\\', "/"); - let relative_path = relative_path.trim_start_matches('/').to_string(); - - if modrinth_path_list.contains(&relative_path) { + let relative_path = ProjectPathId::from_fs_path(&path) + .await? + .get_inner_path_unix(); + if packfile.files.iter().any(|f| f.path == relative_path) + || !included_candidates_set + .contains(&relative_path.get_topmost_two_components()) + { continue; } @@ -696,30 +676,28 @@ pub async fn export_mrpack( Ok(()) } -// Given a folder path, populate a Vec of all the subfolders -// Intended to be used for finding potential override folders +// Given a folder path, populate a Vec of all the subfolders and files, at most 2 layers deep // profile // -- folder1 // -- folder2 +// -- innerfolder +// -- innerfile +// -- folder2file // -- file1 -// => [folder1, folder2] +// => [folder1, folder2/innerfolder, folder2/folder2file, file1] #[tracing::instrument] -pub async fn get_potential_override_folders( - profile_path: ProfilePathId, -) -> crate::Result> { +pub async fn get_pack_export_candidates( + 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 - )) - })?; - // dummy mrpack to get pack list - let mrpack = create_mrpack_json(&profile, "0".to_string(), None).await?; - let mrpack_files = get_modrinth_pack_list(&mrpack); + 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 + )) + })?; - let mut path_list: Vec = Vec::new(); + let mut path_list: Vec = Vec::new(); let profile_base_dir = profile.get_profile_full_path().await?; let mut read_dir = io::read_dir(&profile_base_dir).await?; @@ -738,16 +716,16 @@ pub async fn get_potential_override_folders( .map_err(|e| IOError::with_path(e, &profile_base_dir))? { let path: PathBuf = entry.path(); - 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); + if let Ok(project_path) = + ProjectPathId::from_fs_path(&path).await + { + path_list.push(project_path.get_inner_path_unix()); } } } else { // One layer of files/folders if its a file - 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); + if let Ok(project_path) = ProjectPathId::from_fs_path(&path).await { + path_list.push(project_path.get_inner_path_unix()); } } } @@ -934,19 +912,6 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> { res } -fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec { - packfile - .files - .iter() - .map(|f| { - let path = PathBuf::from(f.path.clone()); - let name = path.to_string_lossy(); - let name = name.replace('\\', "/"); - name.trim_start_matches('/').to_string() - }) - .collect::>() -} - /// 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)] @@ -997,7 +962,7 @@ pub async fn create_mrpack_json( .projects .iter() .filter_map(|(mod_path, project)| { - let path: String = mod_path.get_inner_path_unix().ok()?; + let path = mod_path.get_inner_path_unix(); // Only Modrinth projects have a modrinth metadata field for the modrinth.json Some(Ok(match project.metadata { @@ -1087,7 +1052,7 @@ fn sanitize_loader_version_string(s: &str, loader: PackDependency) -> &str { // Given a folder path, populate a Vec of all the files in the folder, recursively #[async_recursion::async_recursion] -pub async fn build_folder( +pub async fn add_all_recursive_folder_paths( path: &Path, path_list: &mut Vec, ) -> crate::Result<()> { @@ -1099,7 +1064,7 @@ pub async fn build_folder( { let path = entry.path(); if path.is_dir() { - build_folder(&path, path_list).await?; + add_all_recursive_folder_paths(&path, path_list).await?; } else { path_list.push(path); } diff --git a/theseus/src/lib.rs b/theseus/src/lib.rs index c2734d9f..146bafc9 100644 --- a/theseus/src/lib.rs +++ b/theseus/src/lib.rs @@ -22,4 +22,5 @@ pub use api::*; pub use error::*; pub use event::{EventState, LoadingBar, LoadingBarType}; pub use logger::start_logger; +pub use state::InnerProjectPathUnix; pub use state::State; diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 86f07fee..8ef06693 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -383,7 +383,7 @@ pub async fn init_watcher() -> crate::Result> { // 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( + ProfilePathId::new(PathBuf::from( new_path.file_name().unwrap_or_default(), )); diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 50ffeecb..b5e3bdab 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -72,8 +72,8 @@ impl ProfilePathId { } // Create a new ProfilePathId from a relative path - pub fn new(path: &Path) -> Self { - ProfilePathId(PathBuf::from(path)) + pub fn new(path: impl Into) -> Self { + ProfilePathId(path.into()) } pub async fn get_full_path(&self) -> crate::Result { @@ -95,6 +95,45 @@ impl std::fmt::Display for ProfilePathId { } } +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] +#[serde(into = "RawProjectPath", from = "RawProjectPath")] +pub struct InnerProjectPathUnix(pub String); + +impl InnerProjectPathUnix { + pub fn get_topmost_two_components(&self) -> String { + self.to_string() + .split('/') + .take(2) + .collect::>() + .join("/") + } +} + +impl std::fmt::Display for InnerProjectPathUnix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for InnerProjectPathUnix { + fn from(value: RawProjectPath) -> Self { + // Convert windows path to unix path. + // .mrpacks no longer generate windows paths, but this is here for backwards compatibility before this was fixed + // https://github.com/modrinth/theseus/issues/595 + InnerProjectPathUnix(value.0.replace('\\', "/")) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +struct RawProjectPath(pub String); + +impl From for RawProjectPath { + fn from(value: InnerProjectPathUnix) -> Self { + RawProjectPath(value.0) + } +} + /// 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)] @@ -102,11 +141,11 @@ impl std::fmt::Display for ProfilePathId { 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)?; + pub async fn from_fs_path(path: &PathBuf) -> crate::Result { let profiles_dir: PathBuf = io::canonicalize( State::get().await?.directories.profiles_dir().await, )?; + let path: PathBuf = io::canonicalize(path)?; let path = path .strip_prefix(profiles_dir) .ok() @@ -131,13 +170,14 @@ impl ProjectPathId { // Gets inner path in unix convention as a String // ie: 'mods\myproj' -> 'mods/myproj' // Used for exporting to mrpack, which should have a singular convention - pub fn get_inner_path_unix(&self) -> crate::Result { - Ok(self - .0 - .components() - .map(|c| c.as_os_str().to_string_lossy().to_string()) - .collect::>() - .join("/")) + pub fn get_inner_path_unix(&self) -> InnerProjectPathUnix { + InnerProjectPathUnix( + self.0 + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::>() + .join("/"), + ) } // Create a new ProjectPathId from a relative path diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index cab0b782..ac796d77 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -815,7 +815,7 @@ pub async fn infer_data_from_files( 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?; + let h = ProjectPathId::from_fs_path(&h).await?; corrected_hashmap.insert(h, v); } diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index 51480883..9358a56f 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -3,7 +3,7 @@ use daedalus::modded::LoaderVersion; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use theseus::prelude::*; +use theseus::{prelude::*, InnerProjectPathUnix}; use uuid::Uuid; pub fn init() -> tauri::plugin::TauriPlugin { @@ -32,7 +32,7 @@ pub fn init() -> tauri::plugin::TauriPlugin { profile_edit, profile_edit_icon, profile_export_mrpack, - profile_get_potential_override_folders, + profile_get_pack_export_candidates, ]) .build() } @@ -228,20 +228,13 @@ pub async fn profile_export_mrpack( Ok(()) } -// Given a folder path, populate a Vec of all the subfolders -// Intended to be used for finding potential override folders -// profile -// -- folder1 -// -- folder2 -// -- file1 -// => [folder1, folder2] +/// See [`profile::get_pack_export_candidates`] #[tauri::command] -pub async fn profile_get_potential_override_folders( +pub async fn profile_get_pack_export_candidates( profile_path: ProfilePathId, -) -> Result> { - let overrides = - profile::get_potential_override_folders(profile_path).await?; - Ok(overrides) +) -> Result> { + let candidates = profile::get_pack_export_candidates(&profile_path).await?; + Ok(candidates) } // Run minecraft using a profile using the default credentials diff --git a/theseus_gui/src/components/ui/ExportModal.vue b/theseus_gui/src/components/ui/ExportModal.vue index d1bc68df..a9bd402d 100644 --- a/theseus_gui/src/components/ui/ExportModal.vue +++ b/theseus_gui/src/components/ui/ExportModal.vue @@ -2,10 +2,9 @@ import { Button, Checkbox, Modal, XIcon, PlusIcon } from 'omorphia' import { PackageIcon, VersionIcon } from '@/assets/icons' import { ref } from 'vue' -import { export_profile_mrpack, get_potential_override_folders } from '@/helpers/profile.js' +import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js' import { open } from '@tauri-apps/api/dialog' import { handleError } from '@/store/notifications.js' -import { sep } from '@tauri-apps/api/path' import { useTheming } from '@/store/theme' const props = defineProps({ @@ -34,8 +33,9 @@ const themeStore = useTheming() const initFiles = async () => { const newFolders = new Map() + const sep = '/'; files.value = [] - await get_potential_override_folders(props.instance.path).then((filePaths) => + await get_pack_export_candidates(props.instance.path).then((filePaths) => filePaths .map((folder) => ({ path: folder, diff --git a/theseus_gui/src/helpers/profile.js b/theseus_gui/src/helpers/profile.js index af177a80..f4266643 100644 --- a/theseus_gui/src/helpers/profile.js +++ b/theseus_gui/src/helpers/profile.js @@ -151,8 +151,8 @@ export async function export_profile_mrpack( // -- file1 // => [mods, resourcepacks] // allows selection for 'included_overrides' in export_profile_mrpack -export async function get_potential_override_folders(profilePath) { - return await invoke('plugin:profile|profile_get_potential_override_folders', { profilePath }) +export async function get_pack_export_candidates(profilePath) { + return await invoke('plugin:profile|profile_get_pack_export_candidates', { profilePath }) } // Run Minecraft using a pathed profile