diff --git a/Cargo.lock b/Cargo.lock index abdaccd..9207105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,6 +517,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.5.0" @@ -888,10 +894,12 @@ dependencies = [ "colored", "dialoguer", "futures-util", + "hex", "indicatif", "reqwest", "serde", "sha1", + "sha2", "terminal_size", "textwrap", "tokio", @@ -1412,6 +1420,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shell-words" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index e5df991..da5dac3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,12 @@ clap = { version = "4.6.6", features = ["derive"] } colored = "3.1.1" dialoguer = { version = "0.12.0", features = ["fuzzy-select"] } futures-util = "0.3.34" +hex = "0.4.3" indicatif = "0.18.6" reqwest = { version = "0.13.4", features = ["json", "stream", "query"] } serde = { version = "1.0.229", features = ["derive"] } sha1 = "0.11.0" +sha2 = "0.11.0" terminal_size = "0.4.4" textwrap = "0.16.2" tokio = { version = "1.53.1", features = ["full"] } diff --git a/src/commands/install.rs b/src/commands/install.rs new file mode 100644 index 0000000..45b7579 --- /dev/null +++ b/src/commands/install.rs @@ -0,0 +1,108 @@ +use std::error::Error; + +use futures_util::StreamExt; +use tokio::io::AsyncWriteExt; + +use crate::{ + models::{ + config::Config, + install::{InstallQueryParams, InstallResult}, + }, + utils::{verify_sha512, with_progressbar, with_spinner}, +}; + +pub struct Install { + mods: Vec, +} + +impl Install { + const BASE_URL: &str = "https://api.modrinth.com/v2"; + + pub fn new(mods: Vec) -> Self { + Install { mods } + } + + pub async fn run(&self) -> Result<(), Box> { + let file_res = tokio::fs::read_to_string("mcsm.toml").await; + if file_res.is_err() { + return Err("current directory is not a mcsm project".into()); + } + let config: Config = toml::from_str(&file_res.unwrap())?; + + if config.server.loader == "Vanilla" { + return Err("cannot search for mods on Vanilla server".into()); + } + + if std::env::set_current_dir("mods").is_err() { + return Err("mods folder not found".into()); + } + + let client = reqwest::Client::new(); + + for mod_name in &self.mods { + let url = format!("{}/project/{}/version", Self::BASE_URL, mod_name); + + let params = InstallQueryParams { + loaders: format!(r#"["{}"]"#, config.server.loader).to_lowercase(), + game_versions: format!(r#"["{}"]"#, config.server.version), + }; + + let install_result: Vec = with_spinner( + format!("fetching {}", mod_name).as_str(), + format!("fetched {}", mod_name).as_str(), + || async { + let res = client.get(url).query(¶ms).send().await; + + if res.is_err() { + return Err(format!(r#"mod "{}" not found"#, mod_name).into()); + } + + Ok(res.unwrap().json().await?) + }, + ) + .await?; + + let file = install_result.first().unwrap().files.first().unwrap(); + with_progressbar( + format!("downloading {}", mod_name).as_str(), + format!("downloaded {}", mod_name).as_str(), + file.size, + |pb| async move { + let mod_bytes = reqwest::get(&file.url).await?; + let mut file = tokio::fs::File::create(&file.filename).await?; + let mut stream = mod_bytes.bytes_stream(); + + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + file.write_all(&chunk).await?; + pb.inc(chunk.len() as u64); + } + + Ok(()) + }, + ) + .await?; + + with_spinner("verifying sha512 hash", "sha512 hash verified", || async { + let data = tokio::fs::read(&file.filename).await?; + let verified = verify_sha512(&data, &file.hashes.sha512); + + if !verified { + tokio::fs::remove_file("server.jar").await?; + return Err("sha1 verifaction failed for downloaded file".into()); + } + + Ok(()) + }) + .await?; + println!(); + } + + println!( + "{} finished installing mods", + dialoguer::console::style("✔").green() + ); + + Ok(()) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index eede39f..83e620d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod create; +pub mod install; pub mod search; diff --git a/src/main.rs b/src/main.rs index c503db0..6d33756 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ mod commands; mod models; mod utils; -use crate::commands::{create::Create, search::Search}; +use crate::commands::{create::Create, install::Install, search::Search}; use clap::{ Parser, Subcommand, builder::styling::{self}, @@ -53,13 +53,13 @@ enum Commands { #[arg(long)] show_alphas: bool, }, - + /// Search for mods on modrinth for project in current directory /// automticallly filters mod loader, minecraft version from config file Search { /// Search term to look up, e.g. project name or keyword. query: String, - + /// Number of results to skip before returning matches. #[arg(long)] offset: Option, @@ -68,6 +68,13 @@ enum Commands { #[arg(long, value_parser = clap::value_parser!(u32).range(1..=100))] limit: Option, }, + + /// Install mods from modrinth for project in current directory + Install { + /// List of mods to install + #[arg(trailing_var_arg = true)] + mods: Vec, + }, } #[tokio::main] @@ -101,10 +108,18 @@ async fn main() -> Result<(), Box> { let config = create.download_server().await?; create.save_config(config).await?; } - Commands::Search { query, offset, limit } => { + Commands::Search { + query, + offset, + limit, + } => { let search = Search::new(query, offset, limit); search.run().await?; } + Commands::Install { mods } => { + let install = Install::new(mods); + install.run().await?; + } } Ok(()) diff --git a/src/models/install.rs b/src/models/install.rs new file mode 100644 index 0000000..145f29c --- /dev/null +++ b/src/models/install.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +pub struct InstallQueryParams { + pub loaders: String, + pub game_versions: String, +} + +#[derive(Deserialize)] +pub struct InstallResult { + pub version_number: String, + pub files: Vec, +} + +#[derive(Deserialize)] +pub struct InstallFile { + pub hashes: InstallHashes, + pub url: String, + pub filename: String, + pub size: u64, +} + +#[derive(Deserialize)] +pub struct InstallHashes { + pub sha512: String, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 6a75d69..f6747d3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod config; pub mod fabric; +pub mod install; pub mod search; pub mod vanilla;