diff --git a/Cargo.lock b/Cargo.lock index ab452c1..8029be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,9 @@ name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +dependencies = [ + "backtrace", +] [[package]] name = "approx" diff --git a/mcmpmgr/Cargo.toml b/mcmpmgr/Cargo.toml index c4d7e75..8597e13 100644 --- a/mcmpmgr/Cargo.toml +++ b/mcmpmgr/Cargo.toml @@ -5,7 +5,7 @@ version = "0.1.0" edition = "2021" [dependencies] -anyhow = "1.0.86" +anyhow = { version = "1.0.86", features = ["backtrace"] } clap = { version = "4.5.15", features = ["derive"] } git2 = "0.19.0" home = "0.5.9" diff --git a/mcmpmgr/src/file_meta.rs b/mcmpmgr/src/file_meta.rs index 19aed38..27ac9e4 100644 --- a/mcmpmgr/src/file_meta.rs +++ b/mcmpmgr/src/file_meta.rs @@ -1,11 +1,44 @@ use crate::providers::DownloadSide; use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; +use std::{fmt::Display, path::{Path, PathBuf}, str::FromStr}; #[derive(Debug, Clone, Serialize, Deserialize, Hash)] pub struct FileMeta { + /// Relative path of file in the instance folder pub target_path: String, + /// Which side the files should be applied on pub side: DownloadSide, + /// When to apply the files to the instance + pub apply_policy: FileApplyPolicy +} + +#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub enum FileApplyPolicy { + /// Always ensure the file or folder exactly matches that defined in the pack + Always, + /// Only apply the file or folder if it doesn't already exist in the pack + Once +} + +impl FromStr for FileApplyPolicy { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "always" => Ok(Self::Always), + "once" => Ok(Self::Once), + _ => anyhow::bail!("Invalid apply policy {}. Expected one of: always, once", s), + } + } +} + +impl Display for FileApplyPolicy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Always => write!(f, "Always"), + Self::Once => write!(f, "Once"), + } + } } impl PartialEq for FileMeta { diff --git a/mcmpmgr/src/main.rs b/mcmpmgr/src/main.rs index c517014..ad2887a 100644 --- a/mcmpmgr/src/main.rs +++ b/mcmpmgr/src/main.rs @@ -7,7 +7,7 @@ mod resolver; use anyhow::{Error, Result}; use clap::{Args, Parser, Subcommand}; -use file_meta::{get_normalized_relative_path, FileMeta}; +use file_meta::{get_normalized_relative_path, FileApplyPolicy, FileMeta}; use mod_meta::{ModMeta, ModProvider}; use modpack::ModpackMeta; use profiles::{PackSource, Profile}; @@ -134,10 +134,13 @@ enum FileCommands { local_path: PathBuf, /// Target path to copy the file/folder to relative to the MC instance directory #[arg(short, long)] - target_path: Option, + target_path: Option, /// Side to copy the file/folder to #[arg(long, default_value_t = DownloadSide::Server)] side: DownloadSide, + /// File apply policy - whether to always apply the file or just apply it once (if the file doesn't exist) + #[arg(long, default_value_t = FileApplyPolicy::Always)] + apply_policy: FileApplyPolicy, }, /// Show metadata about a file in the pack Show { @@ -437,15 +440,19 @@ async fn main() -> anyhow::Result<()> { local_path, target_path, side, + apply_policy, } => { let mut modpack_meta = ModpackMeta::load_from_current_directory()?; let current_dir = &std::env::current_dir()?; + let target_path = if let Some(target_path) = target_path { + target_path + } else { + get_normalized_relative_path(&local_path, ¤t_dir)? + }; let file_meta = FileMeta { - target_path: get_normalized_relative_path( - &target_path.unwrap_or(local_path.clone()), - current_dir, - )?, + target_path, side, + apply_policy, }; modpack_meta.add_file(&local_path, &file_meta, current_dir)?; @@ -510,10 +517,10 @@ async fn main() -> anyhow::Result<()> { } else { anyhow::bail!("Profile '{name}' does not exist") }; - println!("Profile name : {name}"); + println!("Profile name : {name}"); println!("Instance folder : {}", profile.instance_folder.display()); - println!("Modpack source: {}", profile.pack_source); - println!("Side : {}", profile.side); + println!("Modpack source : {}", profile.pack_source); + println!("Side : {}", profile.side); } } } diff --git a/mcmpmgr/src/modpack.rs b/mcmpmgr/src/modpack.rs index dbc52cc..c0b2a9d 100644 --- a/mcmpmgr/src/modpack.rs +++ b/mcmpmgr/src/modpack.rs @@ -1,6 +1,7 @@ use crate::{ - file_meta::{get_normalized_relative_path, FileMeta}, + file_meta::{get_normalized_relative_path, FileApplyPolicy, FileMeta}, mod_meta::{ModMeta, ModProvider}, + providers::DownloadSide, }; use anyhow::Result; use serde::{Deserialize, Serialize}; @@ -130,16 +131,6 @@ impl ModpackMeta { pack_root.display() ))? }; - - let target_path = PathBuf::from(&file_meta.target_path); - if !target_path.is_relative() { - anyhow::bail!( - "Target path {} for file {} is not relative!", - file_meta.target_path, - file_path.display() - ); - } - let full_path = pack_root.join(relative_path); // Make sure this path is consistent across platforms @@ -189,6 +180,86 @@ impl ModpackMeta { Ok(self) } + /// Installs all the manual files from the pack into the specified directory + /// + /// Files/Folders are added if they don't exist if the policy is set to `FileApplyPolicy::Once`. + /// Otherwise, files/folders are always overwritten. + /// + /// Files/Folders, when applied, will ensure that the exact contents of that file or folder match in the instance folder + /// Ie. If a folder is being applied, any files in that folder not in the modpack will be removed + pub fn install_files( + &self, + pack_dir: &Path, + instance_dir: &Path, + side: DownloadSide, + ) -> Result<()> { + println!( + "Applying modpack files: {} -> {}...", + pack_dir.display(), + instance_dir.display() + ); + if let Some(files) = &self.files { + for (rel_path, file_meta) in files { + let source_path = pack_dir.join(rel_path); + let target_path = instance_dir.join(&file_meta.target_path); + if !side.contains(file_meta.side) { + println!( + "Skipping apply of {} -> {}. (Applies for side={}, current side={})", + source_path.display(), + target_path.display(), + file_meta.side.to_string(), + side.to_string() + ); + continue; + } + if target_path.exists() && file_meta.apply_policy == FileApplyPolicy::Once { + println!( + "Skipping apply of {} -> {}. (Already applied once)", + source_path.display(), + target_path.display(), + ); + continue; + } + + // Otherwise, this file/folder needs to be applied + if source_path.is_dir() { + // Sync a folder + if target_path.exists() { + println!( + "Syncing and overwriting existing directory {} -> {}", + source_path.display(), + target_path.display(), + ); + std::fs::remove_dir_all(&target_path)?; + } + } + self.copy_files(&source_path, &target_path)?; + } + } + Ok(()) + } + + fn copy_files(&self, src: &Path, dst: &Path) -> Result<()> { + if src.is_dir() { + std::fs::create_dir_all(dst)?; + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + self.copy_files(&src_path, &dst_path)?; + } + } else { + let parent_dir = dst.parent(); + if let Some(parent_dir) = parent_dir { + std::fs::create_dir_all(parent_dir)?; + } + println!("Syncing file {} -> {}", src.display(), dst.display()); + std::fs::copy(src, dst)?; + } + + Ok(()) + } + pub fn init_project(&self, directory: &Path) -> Result<()> { let modpack_meta_file_path = directory.join(PathBuf::from(MODPACK_FILENAME)); if modpack_meta_file_path.exists() { diff --git a/mcmpmgr/src/profiles.rs b/mcmpmgr/src/profiles.rs index 64f6a8c..f728fb6 100644 --- a/mcmpmgr/src/profiles.rs +++ b/mcmpmgr/src/profiles.rs @@ -7,7 +7,7 @@ use std::{ str::FromStr, }; -use crate::{providers::DownloadSide, resolver::PinnedPackMeta}; +use crate::{modpack::ModpackMeta, providers::DownloadSide, resolver::PinnedPackMeta}; const CONFIG_DIR_NAME: &str = "mcmpmgr"; const DATA_FILENAME: &str = "data.toml"; @@ -58,16 +58,20 @@ impl Profile { } pub async fn install(&self) -> Result<()> { - let (pack_lock, temp_dir) = match &self.pack_source { + let (pack_lock, pack_directory, _temp_dir) = match &self.pack_source { PackSource::Git { url } => { let (pack_lock, packdir) = PinnedPackMeta::load_from_git_repo(&url, true).await?; - (pack_lock, Some(packdir)) + let pack_path = packdir.path().to_path_buf(); + (pack_lock, pack_path, Some(packdir)) } PackSource::Local { path } => ( PinnedPackMeta::load_from_directory(&path, true).await?, + path.to_path_buf(), None, ), }; + let modpack_meta = ModpackMeta::load_from_directory(&pack_directory)?; + modpack_meta.install_files(&pack_directory, &self.instance_folder, self.side)?; pack_lock .download_mods(&self.instance_folder.join("mods"), self.side) diff --git a/mcmpmgr/src/providers/mod.rs b/mcmpmgr/src/providers/mod.rs index 2a942d7..b5879ca 100644 --- a/mcmpmgr/src/providers/mod.rs +++ b/mcmpmgr/src/providers/mod.rs @@ -28,6 +28,12 @@ pub enum DownloadSide { Client, } +impl DownloadSide { + pub fn contains(self, side: Self) -> bool { + self == Self::Both || side == Self::Both || self == side + } +} + impl FromStr for DownloadSide { type Err = anyhow::Error; diff --git a/mmm/Cargo.toml b/mmm/Cargo.toml index 8fa0fa3..4da261c 100644 --- a/mmm/Cargo.toml +++ b/mmm/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -anyhow = "1.0.86" +anyhow = { version = "1.0.86", features = ["backtrace"] } iced = { version = "0.12.1", features = ["tokio"] } mcmpmgr = { version = "0.1.0", path = "../mcmpmgr" } rfd = "0.14.1"