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,
};
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<PackFileHash, String>,
pub env: Option<HashMap<EnvType, SideType>>,
pub downloads: Vec<String>,

View File

@@ -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?;
}

View File

@@ -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<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>,
description: Option<String>,
_name: Option<String>,
@@ -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::<Vec<_>>();
// 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) {
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<Vec<PathBuf>> {
pub async fn get_pack_export_candidates(
profile_path: &ProfilePathId,
) -> crate::Result<Vec<InnerProjectPathUnix>> {
// 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<PathBuf> = Vec::new();
let mut path_list: Vec<InnerProjectPathUnix> = 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<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
// 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<PathBuf>,
) -> 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);
}

View File

@@ -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;

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
let profile_path_id =
ProfilePathId::new(&PathBuf::from(
ProfilePathId::new(PathBuf::from(
new_path.file_name().unwrap_or_default(),
));

View File

@@ -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<PathBuf>) -> Self {
ProfilePathId(path.into())
}
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
/// 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<Self> {
let path: PathBuf = io::canonicalize(path)?;
pub async fn from_fs_path(path: &PathBuf) -> crate::Result<Self> {
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<String> {
Ok(self
.0
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.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::<Vec<_>>()
.join("/"),
)
}
// 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 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);
}

View File

@@ -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<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_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<Vec<PathBuf>> {
let overrides =
profile::get_potential_override_folders(profile_path).await?;
Ok(overrides)
) -> Result<Vec<InnerProjectPathUnix>> {
let candidates = profile::get_pack_export_candidates(&profile_path).await?;
Ok(candidates)
}
// Run minecraft using a profile using the default credentials

View File

@@ -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,

View File

@@ -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