forked from didirus/AstralRinth
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:
@@ -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>,
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user