Compare commits
No commits in common. "03aadbe9776b02701f075f21bbe2043ad0f1acbe" and "24cd3d2765fd072e15e82fe559468fd045ec6dad" have entirely different histories.
03aadbe977
...
24cd3d2765
11 changed files with 35 additions and 229 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
|
@ -517,12 +517,6 @@ 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"
|
||||
|
|
@ -894,12 +888,10 @@ dependencies = [
|
|||
"colored",
|
||||
"dialoguer",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"indicatif",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"terminal_size",
|
||||
"textwrap",
|
||||
"tokio",
|
||||
|
|
@ -1420,17 +1412,6 @@ 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"
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ 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"] }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::error::Error;
|
|||
|
||||
use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme};
|
||||
use futures_util::StreamExt;
|
||||
use sha1::{Digest, Sha1};
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -10,7 +11,7 @@ use crate::{
|
|||
fabric::FabricVersions,
|
||||
vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions},
|
||||
},
|
||||
utils::{verify_sha1, with_progressbar, with_spinner},
|
||||
utils::{with_progressbar, with_spinner},
|
||||
};
|
||||
|
||||
pub struct Create {
|
||||
|
|
@ -171,10 +172,12 @@ impl LoaderDownloader for VanillaDownloader {
|
|||
|
||||
with_spinner("verifying sha1 hash", "sha1 hash verified", || async {
|
||||
let data = tokio::fs::read("server.jar").await?;
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(&data);
|
||||
let hash = hasher.finalize();
|
||||
let hex: String = hash.iter().map(|b| format!("{:02x}", b)).collect();
|
||||
|
||||
let verified = verify_sha1(&data, &version_detail.downloads.server.sha1);
|
||||
|
||||
if !verified {
|
||||
if hex != version_detail.downloads.server.sha1 {
|
||||
tokio::fs::remove_file("server.jar").await?;
|
||||
return Err("sha1 verifaction failed for downloaded file".into());
|
||||
}
|
||||
|
|
@ -294,8 +297,6 @@ impl LoaderDownloader for FabricDownloader {
|
|||
})
|
||||
.await?;
|
||||
|
||||
tokio::fs::create_dir("mods").await?;
|
||||
|
||||
let config = Config {
|
||||
server: Server {
|
||||
version: create.version.clone().unwrap(),
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
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<String>,
|
||||
}
|
||||
|
||||
impl Install {
|
||||
const BASE_URL: &str = "https://api.modrinth.com/v2";
|
||||
|
||||
pub fn new(mods: Vec<String>) -> Self {
|
||||
Install { mods }
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<(), Box<dyn Error>> {
|
||||
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<InstallResult> = 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
pub mod create;
|
||||
pub mod install;
|
||||
pub mod search;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
use std::error::Error;
|
||||
|
||||
use colored::Colorize;
|
||||
use serde::Serialize;
|
||||
use terminal_size;
|
||||
use terminal_size::{Width, terminal_size as get_terminal_size};
|
||||
use textwrap::{Options, fill};
|
||||
use terminal_size::{terminal_size as get_terminal_size, Width};
|
||||
|
||||
use crate::models::{
|
||||
config::Config,
|
||||
search::{SearchQueryParams, SearchResult},
|
||||
};
|
||||
use crate::models::{config::Config, search::SearchResult};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SearchQueryParams {
|
||||
query: String,
|
||||
facets: String,
|
||||
offset: Option<u32>,
|
||||
limit: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct Search {
|
||||
query: String,
|
||||
|
|
@ -19,12 +25,12 @@ pub struct Search {
|
|||
impl Search {
|
||||
const BASE_URL: &str = "https://api.modrinth.com/v2";
|
||||
|
||||
pub fn new(query: String, offset: Option<u32>, limit: Option<u32>) -> Self {
|
||||
Search {
|
||||
query,
|
||||
offset,
|
||||
limit,
|
||||
}
|
||||
pub fn new(
|
||||
query: String,
|
||||
offset: Option<u32>,
|
||||
limit: Option<u32>,
|
||||
) -> Self {
|
||||
Search { query, offset, limit }
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<(), Box<dyn Error>> {
|
||||
|
|
@ -60,7 +66,7 @@ impl Search {
|
|||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
|
||||
self.print_results(&res);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -69,18 +75,12 @@ impl Search {
|
|||
fn print_results(&self, res: &SearchResult) {
|
||||
println!(
|
||||
"{}\n",
|
||||
format!(
|
||||
"Found {} result{}",
|
||||
res.total_hits,
|
||||
if res.total_hits == 1 { "" } else { "s" }
|
||||
)
|
||||
.green()
|
||||
.bold()
|
||||
format!("Found {} result{}", res.total_hits, if res.total_hits == 1 { "" } else { "s" })
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
|
||||
let width = get_terminal_size()
|
||||
.map(|(Width(w), _)| w as usize)
|
||||
.unwrap_or(80);
|
||||
let width = get_terminal_size().map(|(Width(w), _)| w as usize).unwrap_or(80);
|
||||
|
||||
for (i, hit) in res.hits.iter().enumerate() {
|
||||
let wrapped = fill(
|
||||
|
|
|
|||
23
src/main.rs
23
src/main.rs
|
|
@ -2,7 +2,7 @@ mod commands;
|
|||
mod models;
|
||||
mod utils;
|
||||
|
||||
use crate::commands::{create::Create, install::Install, search::Search};
|
||||
use crate::commands::{create::Create, 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<u32>,
|
||||
|
|
@ -68,13 +68,6 @@ enum Commands {
|
|||
#[arg(long, value_parser = clap::value_parser!(u32).range(1..=100))]
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Install mods from modrinth for project in current directory
|
||||
Install {
|
||||
/// List of mods to install
|
||||
#[arg(trailing_var_arg = true)]
|
||||
mods: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -108,18 +101,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
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(())
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
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<InstallFile>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
pub mod config;
|
||||
pub mod fabric;
|
||||
pub mod install;
|
||||
pub mod search;
|
||||
pub mod vanilla;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SearchQueryParams {
|
||||
pub query: String,
|
||||
pub facets: String,
|
||||
pub offset: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SearchResult {
|
||||
|
|
|
|||
17
src/utils.rs
17
src/utils.rs
|
|
@ -1,6 +1,4 @@
|
|||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use sha1::{Digest, Sha1};
|
||||
use sha2::Sha512;
|
||||
use std::{error::Error, future::Future};
|
||||
|
||||
pub async fn with_spinner<F, Fut, T>(
|
||||
|
|
@ -44,20 +42,7 @@ where
|
|||
pb.set_message(start_msg.to_string());
|
||||
|
||||
let result = f(pb.clone()).await;
|
||||
let formatted_finish_msg = format!("{} {}", dialoguer::console::style("✔").green(), finish_msg);
|
||||
pb.finish_with_message(formatted_finish_msg);
|
||||
pb.finish_with_message(finish_msg.to_string());
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn verify_sha1(data: &[u8], expected_hex: &str) -> bool {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(data);
|
||||
hex::encode(hasher.finalize()) == expected_hex
|
||||
}
|
||||
|
||||
pub fn verify_sha512(data: &[u8], expected_hex: &str) -> bool {
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(data);
|
||||
hex::encode(hasher.finalize()) == expected_hex
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue