diff --git a/Cargo.lock b/Cargo.lock index 21e7222..ab452c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2067,6 +2067,7 @@ dependencies = [ "clap", "git2", "home", + "pathdiff", "reqwest", "semver", "serde", @@ -2618,6 +2619,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/mcmpmgr/Cargo.toml b/mcmpmgr/Cargo.toml index b257188..c4d7e75 100644 --- a/mcmpmgr/Cargo.toml +++ b/mcmpmgr/Cargo.toml @@ -9,6 +9,7 @@ anyhow = "1.0.86" clap = { version = "4.5.15", features = ["derive"] } git2 = "0.19.0" home = "0.5.9" +pathdiff = "0.2.1" reqwest = { version = "0.12.5", features = ["json"] } semver = { version = "1.0.23", features = ["serde"] } serde = { version = "1.0.207", features = ["derive"] } diff --git a/mcmpmgr/src/file_meta.rs b/mcmpmgr/src/file_meta.rs new file mode 100644 index 0000000..19aed38 --- /dev/null +++ b/mcmpmgr/src/file_meta.rs @@ -0,0 +1,64 @@ +use crate::providers::DownloadSide; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] +pub struct FileMeta { + pub target_path: String, + pub side: DownloadSide, +} + +impl PartialEq for FileMeta { + fn eq(&self, other: &Self) -> bool { + self.target_path == other.target_path + } +} + +impl PartialOrd for FileMeta { + fn partial_cmp(&self, other: &Self) -> Option { + self.target_path.partial_cmp(&other.target_path) + } +} + +impl Ord for FileMeta { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.target_path.cmp(&other.target_path) + } +} + +impl Eq for FileMeta {} + +/// Get a normalized relative path string in a consistent way across platforms +/// TODO: Make a nice struct for this maybe +pub fn get_normalized_relative_path( + path_to_normalize: &Path, + base_path: &Path, +) -> anyhow::Result { + if path_to_normalize.is_absolute() { + anyhow::bail!( + "Absolute paths are not supported! Will not normalise {}", + path_to_normalize.display() + ); + } + let base_path = base_path.canonicalize()?; + let full_path = base_path.join(path_to_normalize).canonicalize()?; + let relative_path = pathdiff::diff_paths(&full_path, &base_path).ok_or(anyhow::format_err!( + "Cannot normalize path {} relative to {}", + &path_to_normalize.display(), + &base_path.display() + ))?; + + let mut normalized_path = String::new(); + for (i, component) in relative_path.components().enumerate() { + if i > 0 { + normalized_path.push('/'); + } + normalized_path.push_str(&component.as_os_str().to_string_lossy()); + } + + if !normalized_path.starts_with("./") && !normalized_path.starts_with("/") { + normalized_path.insert_str(0, "./"); + } + + Ok(normalized_path) +} diff --git a/mcmpmgr/src/lib.rs b/mcmpmgr/src/lib.rs index 2351c11..e364a04 100644 --- a/mcmpmgr/src/lib.rs +++ b/mcmpmgr/src/lib.rs @@ -1,5 +1,6 @@ pub mod mod_meta; +pub mod file_meta; pub mod modpack; pub mod profiles; pub mod providers; -pub mod resolver; \ No newline at end of file +pub mod resolver; diff --git a/mcmpmgr/src/main.rs b/mcmpmgr/src/main.rs index eb56bef..c517014 100644 --- a/mcmpmgr/src/main.rs +++ b/mcmpmgr/src/main.rs @@ -1,3 +1,4 @@ +mod file_meta; mod mod_meta; mod modpack; mod profiles; @@ -6,6 +7,7 @@ mod resolver; use anyhow::{Error, Result}; use clap::{Args, Parser, Subcommand}; +use file_meta::{get_normalized_relative_path, FileMeta}; use mod_meta::{ModMeta, ModProvider}; use modpack::ModpackMeta; use profiles::{PackSource, Profile}; @@ -53,7 +55,7 @@ enum Commands { #[arg(long)] providers: Vec, }, - /// Add a new mod or to the modpack + /// Add a new mod to the modpack Add { /// Name of the mod to add to the project, optionally including a version name: String, @@ -74,7 +76,7 @@ enum Commands { modloader: Option, /// Side override #[arg(long, short)] - side: Option + side: Option, }, /// Remove a mod from the modpack Remove { @@ -109,10 +111,46 @@ enum Commands { #[arg(long, short, action)] locked: bool, }, + /// Manage local files in the modpack + File(FileArgs), /// Manage mcmpmgr profiles Profile(ProfileArgs), } +#[derive(Debug, Args)] +#[command(args_conflicts_with_subcommands = true)] +struct FileArgs { + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +enum FileCommands { + /// List all files/folders in the pack + List, + /// Add new files/folder to the pack + Add { + /// Local path to file/folder to include in the pack (must be in the pack root) + local_path: PathBuf, + /// Target path to copy the file/folder to relative to the MC instance directory + #[arg(short, long)] + target_path: Option, + /// Side to copy the file/folder to + #[arg(long, default_value_t = DownloadSide::Server)] + side: DownloadSide, + }, + /// Show metadata about a file in the pack + Show { + /// Local path of the file/folder to show + local_path: String, + }, + /// Remove a file/folder from the pack + Remove { + /// local path to file/folder to remove + local_path: PathBuf, + }, +} + #[derive(Debug, Args)] #[command(args_conflicts_with_subcommands = true)] struct ProfileArgs { @@ -228,7 +266,7 @@ async fn main() -> anyhow::Result<()> { locked, mc_version, modloader, - side + side, } => { let mut modpack_meta = ModpackMeta::load_from_current_directory()?; let old_modpack_meta = modpack_meta.clone(); @@ -251,15 +289,15 @@ async fn main() -> anyhow::Result<()> { DownloadSide::Both => { mod_meta.server_side = Some(true); mod_meta.client_side = Some(true); - }, + } DownloadSide::Server => { mod_meta.server_side = Some(true); mod_meta.client_side = Some(false); - }, + } DownloadSide::Client => { mod_meta.server_side = Some(false); mod_meta.client_side = Some(true); - }, + } } } for provider in providers.into_iter() { @@ -391,6 +429,37 @@ async fn main() -> anyhow::Result<()> { pack_lock.init(&modpack_meta, !locked).await?; pack_lock.save_current_dir_lock()?; } + Commands::File(FileArgs { command }) => { + if let Some(command) = command { + match command { + FileCommands::List => todo!(), + FileCommands::Add { + local_path, + target_path, + side, + } => { + let mut modpack_meta = ModpackMeta::load_from_current_directory()?; + let current_dir = &std::env::current_dir()?; + let file_meta = FileMeta { + target_path: get_normalized_relative_path( + &target_path.unwrap_or(local_path.clone()), + current_dir, + )?, + side, + }; + + modpack_meta.add_file(&local_path, &file_meta, current_dir)?; + modpack_meta.save_current_dir_project()?; + } + FileCommands::Show { local_path } => todo!(), + FileCommands::Remove { local_path } => { + let mut modpack_meta = ModpackMeta::load_from_current_directory()?; + modpack_meta.remove_file(&local_path, &std::env::current_dir()?)?; + modpack_meta.save_current_dir_project()?; + } + } + } + } Commands::Profile(ProfileArgs { command }) => { if let Some(command) = command { match command { diff --git a/mcmpmgr/src/modpack.rs b/mcmpmgr/src/modpack.rs index a654f2d..dbc52cc 100644 --- a/mcmpmgr/src/modpack.rs +++ b/mcmpmgr/src/modpack.rs @@ -1,4 +1,7 @@ -use crate::mod_meta::{ModMeta, ModProvider}; +use crate::{ + file_meta::{get_normalized_relative_path, FileMeta}, + mod_meta::{ModMeta, ModProvider}, +}; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::{ @@ -38,11 +41,19 @@ impl std::str::FromStr for ModLoader { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ModpackMeta { + /// The name of the modpack pub pack_name: String, + /// The intended minecraft version on which this pack should run pub mc_version: String, + /// The default modloader for the modpack pub modloader: ModLoader, + /// Map of mod name -> mod metadata pub mods: BTreeMap, + /// Mapping of relative paths to files to copy over from the modpack + pub files: Option>, + /// Default provider for newly added mods in the modpack pub default_providers: Vec, + /// A set of forbidden mods in the modpack pub forbidden_mods: BTreeSet, } @@ -103,6 +114,81 @@ impl ModpackMeta { self } + /// Add local files or folders to the pack. These should be committed to version control + pub fn add_file( + &mut self, + file_path: &Path, + file_meta: &FileMeta, + pack_root: &Path, + ) -> Result<&mut Self> { + let relative_path = if file_path.is_relative() { + file_path + } else { + &pathdiff::diff_paths(file_path, pack_root).ok_or(anyhow::format_err!( + "Cannot get relative path of {} in {}", + file_path.display(), + 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 + let relative_path = get_normalized_relative_path(relative_path, &pack_root)?; + + if !full_path + .canonicalize()? + .starts_with(pack_root.canonicalize()?) + { + anyhow::bail!( + "You cannot add local files to the modpack from outside the pack source directory. {} is not contained in {}", + full_path.canonicalize()?.display(), + pack_root.canonicalize()?.display() + ); + } + + match &mut self.files { + Some(files) => { + files.insert(relative_path.clone(), file_meta.clone()); + } + None => { + self.files + .insert(BTreeMap::new()) + .insert(relative_path.clone(), file_meta.clone()); + } + } + + println!( + "Added file '{relative_path}' -> '{}' to modpack...", + file_meta.target_path + ); + + Ok(self) + } + + pub fn remove_file(&mut self, file_path: &PathBuf, pack_root: &Path) -> Result<&mut Self> { + let relative_path = get_normalized_relative_path(&file_path, pack_root)?; + if let Some(files) = &mut self.files { + let removed = files.remove(&relative_path); + if let Some(removed) = removed { + println!( + "Removed file '{relative_path}' -> '{}' from modpack...", + removed.target_path + ); + } + } + Ok(self) + } + 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() { @@ -140,6 +226,7 @@ impl std::default::Default for ModpackMeta { mc_version: "1.20.1".into(), modloader: ModLoader::Forge, mods: Default::default(), + files: Default::default(), default_providers: vec![ModProvider::Modrinth], forbidden_mods: Default::default(), } diff --git a/mcmpmgr/src/providers/mod.rs b/mcmpmgr/src/providers/mod.rs index ec898cf..2a942d7 100644 --- a/mcmpmgr/src/providers/mod.rs +++ b/mcmpmgr/src/providers/mod.rs @@ -21,7 +21,7 @@ pub enum FileSource { }, } -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Hash)] pub enum DownloadSide { Both, Server,