Make export selection consistent between platforms and allow selecting which projects to export (#789)

* Experimenting with tests

* Overhaul handling of paths for pack files to always use standardized style

Also allows disabling export of all items

* Minor improvements

* Revert test things

* Minor tweaks

* Fix clippy warning
This commit is contained in:
Jackson Kruger
2023-10-09 12:34:19 -05:00
committed by GitHub
parent e76a7d57c0
commit 772597ce2a
10 changed files with 117 additions and 118 deletions

View File

@@ -10,7 +10,7 @@ use crate::util::fetch::{
fetch, fetch_advanced, fetch_json, write_cached_icon, fetch, fetch_advanced, fetch_json, write_cached_icon,
}; };
use crate::util::io; use crate::util::io;
use crate::State; use crate::{InnerProjectPathUnix, State};
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -33,7 +33,7 @@ pub struct PackFormat {
#[derive(Serialize, Deserialize, Eq, PartialEq)] #[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PackFile { pub struct PackFile {
pub path: String, pub path: InnerProjectPathUnix,
pub hashes: HashMap<PackFileHash, String>, pub hashes: HashMap<PackFileHash, String>,
pub env: Option<HashMap<EnvType, SideType>>, pub env: Option<HashMap<EnvType, SideType>>,
pub downloads: Vec<String>, pub downloads: Vec<String>,

View File

@@ -189,10 +189,7 @@ pub async fn install_zipped_mrpack_files(
.await?; .await?;
drop(creds); drop(creds);
// Convert windows path to unix path. let project_path = project.path.to_string();
// .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 path = let path =
std::path::Path::new(&project_path).components().next(); 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 // 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) // (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
for file in pack.files { 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() { if path.exists() {
io::remove_file(&path).await?; io::remove_file(&path).await?;
} }

View File

@@ -8,7 +8,7 @@ use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat, EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
}; };
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
use crate::state::{ProjectMetadata, SideType}; use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType};
use crate::util::fetch; use crate::util::fetch;
use crate::util::io::{self, IOError}; use crate::util::io::{self, IOError};
@@ -25,8 +25,9 @@ use async_zip::tokio::write::ZipFileWriter;
use async_zip::{Compression, ZipEntryBuilder}; use async_zip::{Compression, ZipEntryBuilder};
use serde_json::json; use serde_json::json;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use std::{ use std::{
future::Future, future::Future,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -570,7 +571,7 @@ pub async fn remove_project(
pub async fn export_mrpack( pub async fn export_mrpack(
profile_path: &ProfilePathId, profile_path: &ProfilePathId,
export_path: PathBuf, export_path: PathBuf,
included_overrides: Vec<String>, // which folders to include in the overrides included_export_candidates: Vec<String>, // which folders/files to include in the export
version_id: Option<String>, version_id: Option<String>,
description: Option<String>, description: Option<String>,
_name: Option<String>, _name: Option<String>,
@@ -585,8 +586,8 @@ pub async fn export_mrpack(
)) ))
})?; })?;
// remove .DS_Store files from included_overrides // remove .DS_Store files from included_export_candidates
let included_overrides = included_overrides let included_export_candidates = included_export_candidates
.into_iter() .into_iter()
.filter(|x| { .filter(|x| {
if let Some(f) = PathBuf::from(x).file_name() { if let Some(f) = PathBuf::from(x).file_name() {
@@ -607,13 +608,17 @@ pub async fn export_mrpack(
// Create mrpack json configuration file // Create mrpack json configuration file
let version_id = version_id.unwrap_or("1.0.0".to_string()); 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?; 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 // Build vec of all files in the folder
let mut path_list = Vec::new(); 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 // Initialize loading bar
let loading_bar = init_loading( let loading_bar = init_loading(
@@ -631,38 +636,13 @@ pub async fn export_mrpack(
for path in path_list { for path in path_list {
emit_loading(&loading_bar, 1.0, None).await?; emit_loading(&loading_bar, 1.0, None).await?;
// Get local path of file, relative to profile folder let relative_path = ProjectPathId::from_fs_path(&path)
let relative_path = path.strip_prefix(profile_base_path)?; .await?
.get_inner_path_unix();
// Get highest level folder pair ('a/b' in 'a/b/c', 'a' in 'a') if packfile.files.iter().any(|f| f.path == relative_path)
// We only go one layer deep for the sake of not having a huge list of overrides || !included_candidates_set
let topmost_two = relative_path.iter().take(2).collect::<Vec<_>>(); .contains(&relative_path.get_topmost_two_components())
{
// 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<str> =
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) {
continue; continue;
} }
@@ -696,30 +676,28 @@ pub async fn export_mrpack(
Ok(()) Ok(())
} }
// Given a folder path, populate a Vec of all the subfolders // Given a folder path, populate a Vec of all the subfolders and files, at most 2 layers deep
// Intended to be used for finding potential override folders
// profile // profile
// -- folder1 // -- folder1
// -- folder2 // -- folder2
// -- innerfolder
// -- innerfile
// -- folder2file
// -- file1 // -- file1
// => [folder1, folder2] // => [folder1, folder2/innerfolder, folder2/folder2file, file1]
#[tracing::instrument] #[tracing::instrument]
pub async fn get_potential_override_folders( pub async fn get_pack_export_candidates(
profile_path: ProfilePathId, profile_path: &ProfilePathId,
) -> crate::Result<Vec<PathBuf>> { ) -> crate::Result<Vec<InnerProjectPathUnix>> {
// First, get a dummy mrpack json for the files within // First, get a dummy mrpack json for the files within
let profile: Profile = let profile: Profile = get(profile_path, None).await?.ok_or_else(|| {
get(&profile_path, None).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!(
crate::ErrorKind::OtherError(format!( "Tried to export a nonexistent or unloaded profile at path {}!",
"Tried to export a nonexistent or unloaded profile at path {}!", profile_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 mut path_list: Vec<PathBuf> = Vec::new(); let mut path_list: Vec<InnerProjectPathUnix> = Vec::new();
let profile_base_dir = profile.get_profile_full_path().await?; let profile_base_dir = profile.get_profile_full_path().await?;
let mut read_dir = io::read_dir(&profile_base_dir).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))? .map_err(|e| IOError::with_path(e, &profile_base_dir))?
{ {
let path: PathBuf = entry.path(); let path: PathBuf = entry.path();
let name = path.strip_prefix(&profile_base_dir)?.to_path_buf(); if let Ok(project_path) =
if !mrpack_files.contains(&name.to_string_lossy().to_string()) { ProjectPathId::from_fs_path(&path).await
path_list.push(name); {
path_list.push(project_path.get_inner_path_unix());
} }
} }
} else { } else {
// One layer of files/folders if its a file // One layer of files/folders if its a file
let name = path.strip_prefix(&profile_base_dir)?.to_path_buf(); if let Ok(project_path) = ProjectPathId::from_fs_path(&path).await {
if !mrpack_files.contains(&name.to_string_lossy().to_string()) { path_list.push(project_path.get_inner_path_unix());
path_list.push(name);
} }
} }
} }
@@ -934,19 +912,6 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
res res
} }
fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec<String> {
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::<Vec<String>>()
}
/// Creates a json configuration for a .mrpack zipped file /// 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) // Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44)
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@@ -997,7 +962,7 @@ pub async fn create_mrpack_json(
.projects .projects
.iter() .iter()
.filter_map(|(mod_path, project)| { .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 // Only Modrinth projects have a modrinth metadata field for the modrinth.json
Some(Ok(match project.metadata { 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 // Given a folder path, populate a Vec of all the files in the folder, recursively
#[async_recursion::async_recursion] #[async_recursion::async_recursion]
pub async fn build_folder( pub async fn add_all_recursive_folder_paths(
path: &Path, path: &Path,
path_list: &mut Vec<PathBuf>, path_list: &mut Vec<PathBuf>,
) -> crate::Result<()> { ) -> crate::Result<()> {
@@ -1099,7 +1064,7 @@ pub async fn build_folder(
{ {
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
build_folder(&path, path_list).await?; add_all_recursive_folder_paths(&path, path_list).await?;
} else { } else {
path_list.push(path); path_list.push(path);
} }

View File

@@ -22,4 +22,5 @@ pub use api::*;
pub use error::*; pub use error::*;
pub use event::{EventState, LoadingBar, LoadingBarType}; pub use event::{EventState, LoadingBar, LoadingBarType};
pub use logger::start_logger; pub use logger::start_logger;
pub use state::InnerProjectPathUnix;
pub use state::State; pub use state::State;

View File

@@ -383,7 +383,7 @@ pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
// At this point, new_path is the path to the profile, and subfile is whether it's a subfile of the profile or not // 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 = let profile_path_id =
ProfilePathId::new(&PathBuf::from( ProfilePathId::new(PathBuf::from(
new_path.file_name().unwrap_or_default(), new_path.file_name().unwrap_or_default(),
)); ));

View File

@@ -72,8 +72,8 @@ impl ProfilePathId {
} }
// Create a new ProfilePathId from a relative path // Create a new ProfilePathId from a relative path
pub fn new(path: &Path) -> Self { pub fn new(path: impl Into<PathBuf>) -> Self {
ProfilePathId(PathBuf::from(path)) ProfilePathId(path.into())
} }
pub async fn get_full_path(&self) -> crate::Result<PathBuf> { pub async fn get_full_path(&self) -> crate::Result<PathBuf> {
@@ -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::<Vec<_>>()
.join("/")
}
}
impl std::fmt::Display for InnerProjectPathUnix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<RawProjectPath> 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<InnerProjectPathUnix> 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 /// 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" /// eg: for "a/b/c/profiles/My Mod/mods/myproj", the ProjectPathId would be "mods/myproj"
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
@@ -102,11 +141,11 @@ impl std::fmt::Display for ProfilePathId {
pub struct ProjectPathId(pub PathBuf); pub struct ProjectPathId(pub PathBuf);
impl ProjectPathId { impl ProjectPathId {
// Create a new ProjectPathId from a full file path // Create a new ProjectPathId from a full file path
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> { pub async fn from_fs_path(path: &PathBuf) -> crate::Result<Self> {
let path: PathBuf = io::canonicalize(path)?;
let profiles_dir: PathBuf = io::canonicalize( let profiles_dir: PathBuf = io::canonicalize(
State::get().await?.directories.profiles_dir().await, State::get().await?.directories.profiles_dir().await,
)?; )?;
let path: PathBuf = io::canonicalize(path)?;
let path = path let path = path
.strip_prefix(profiles_dir) .strip_prefix(profiles_dir)
.ok() .ok()
@@ -131,13 +170,14 @@ impl ProjectPathId {
// Gets inner path in unix convention as a String // Gets inner path in unix convention as a String
// ie: 'mods\myproj' -> 'mods/myproj' // ie: 'mods\myproj' -> 'mods/myproj'
// Used for exporting to mrpack, which should have a singular convention // Used for exporting to mrpack, which should have a singular convention
pub fn get_inner_path_unix(&self) -> crate::Result<String> { pub fn get_inner_path_unix(&self) -> InnerProjectPathUnix {
Ok(self InnerProjectPathUnix(
.0 self.0
.components() .components()
.map(|c| c.as_os_str().to_string_lossy().to_string()) .map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("/")) .join("/"),
)
} }
// Create a new ProjectPathId from a relative path // Create a new ProjectPathId from a relative path

View File

@@ -815,7 +815,7 @@ pub async fn infer_data_from_files(
let mut corrected_hashmap = HashMap::new(); let mut corrected_hashmap = HashMap::new();
let mut stream = tokio_stream::iter(return_projects); let mut stream = tokio_stream::iter(return_projects);
while let Some((h, v)) = stream.next().await { 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); corrected_hashmap.insert(h, v);
} }

View File

@@ -3,7 +3,7 @@ use daedalus::modded::LoaderVersion;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use theseus::prelude::*; use theseus::{prelude::*, InnerProjectPathUnix};
use uuid::Uuid; use uuid::Uuid;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
@@ -32,7 +32,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
profile_edit, profile_edit,
profile_edit_icon, profile_edit_icon,
profile_export_mrpack, profile_export_mrpack,
profile_get_potential_override_folders, profile_get_pack_export_candidates,
]) ])
.build() .build()
} }
@@ -228,20 +228,13 @@ pub async fn profile_export_mrpack(
Ok(()) Ok(())
} }
// Given a folder path, populate a Vec of all the subfolders /// See [`profile::get_pack_export_candidates`]
// Intended to be used for finding potential override folders
// profile
// -- folder1
// -- folder2
// -- file1
// => [folder1, folder2]
#[tauri::command] #[tauri::command]
pub async fn profile_get_potential_override_folders( pub async fn profile_get_pack_export_candidates(
profile_path: ProfilePathId, profile_path: ProfilePathId,
) -> Result<Vec<PathBuf>> { ) -> Result<Vec<InnerProjectPathUnix>> {
let overrides = let candidates = profile::get_pack_export_candidates(&profile_path).await?;
profile::get_potential_override_folders(profile_path).await?; Ok(candidates)
Ok(overrides)
} }
// Run minecraft using a profile using the default credentials // Run minecraft using a profile using the default credentials

View File

@@ -2,10 +2,9 @@
import { Button, Checkbox, Modal, XIcon, PlusIcon } from 'omorphia' import { Button, Checkbox, Modal, XIcon, PlusIcon } from 'omorphia'
import { PackageIcon, VersionIcon } from '@/assets/icons' import { PackageIcon, VersionIcon } from '@/assets/icons'
import { ref } from 'vue' 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 { open } from '@tauri-apps/api/dialog'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { sep } from '@tauri-apps/api/path'
import { useTheming } from '@/store/theme' import { useTheming } from '@/store/theme'
const props = defineProps({ const props = defineProps({
@@ -34,8 +33,9 @@ const themeStore = useTheming()
const initFiles = async () => { const initFiles = async () => {
const newFolders = new Map() const newFolders = new Map()
const sep = '/';
files.value = [] files.value = []
await get_potential_override_folders(props.instance.path).then((filePaths) => await get_pack_export_candidates(props.instance.path).then((filePaths) =>
filePaths filePaths
.map((folder) => ({ .map((folder) => ({
path: folder, path: folder,

View File

@@ -151,8 +151,8 @@ export async function export_profile_mrpack(
// -- file1 // -- file1
// => [mods, resourcepacks] // => [mods, resourcepacks]
// allows selection for 'included_overrides' in export_profile_mrpack // allows selection for 'included_overrides' in export_profile_mrpack
export async function get_potential_override_folders(profilePath) { export async function get_pack_export_candidates(profilePath) {
return await invoke('plugin:profile|profile_get_potential_override_folders', { profilePath }) return await invoke('plugin:profile|profile_get_pack_export_candidates', { profilePath })
} }
// Run Minecraft using a pathed profile // Run Minecraft using a pathed profile