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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hex"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -894,12 +888,10 @@ dependencies = [
|
||||||
"colored",
|
"colored",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2",
|
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -1420,17 +1412,6 @@ dependencies = [
|
||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
name = "shell-words"
|
name = "shell-words"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,10 @@ clap = { version = "4.6.6", features = ["derive"] }
|
||||||
colored = "3.1.1"
|
colored = "3.1.1"
|
||||||
dialoguer = { version = "0.12.0", features = ["fuzzy-select"] }
|
dialoguer = { version = "0.12.0", features = ["fuzzy-select"] }
|
||||||
futures-util = "0.3.34"
|
futures-util = "0.3.34"
|
||||||
hex = "0.4.3"
|
|
||||||
indicatif = "0.18.6"
|
indicatif = "0.18.6"
|
||||||
reqwest = { version = "0.13.4", features = ["json", "stream", "query"] }
|
reqwest = { version = "0.13.4", features = ["json", "stream", "query"] }
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
sha1 = "0.11.0"
|
sha1 = "0.11.0"
|
||||||
sha2 = "0.11.0"
|
|
||||||
terminal_size = "0.4.4"
|
terminal_size = "0.4.4"
|
||||||
textwrap = "0.16.2"
|
textwrap = "0.16.2"
|
||||||
tokio = { version = "1.53.1", features = ["full"] }
|
tokio = { version = "1.53.1", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::error::Error;
|
||||||
|
|
||||||
use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme};
|
use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
use tokio::{fs::File, io::AsyncWriteExt};
|
use tokio::{fs::File, io::AsyncWriteExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -10,7 +11,7 @@ use crate::{
|
||||||
fabric::FabricVersions,
|
fabric::FabricVersions,
|
||||||
vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions},
|
vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions},
|
||||||
},
|
},
|
||||||
utils::{verify_sha1, with_progressbar, with_spinner},
|
utils::{with_progressbar, with_spinner},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Create {
|
pub struct Create {
|
||||||
|
|
@ -171,10 +172,12 @@ impl LoaderDownloader for VanillaDownloader {
|
||||||
|
|
||||||
with_spinner("verifying sha1 hash", "sha1 hash verified", || async {
|
with_spinner("verifying sha1 hash", "sha1 hash verified", || async {
|
||||||
let data = tokio::fs::read("server.jar").await?;
|
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 hex != version_detail.downloads.server.sha1 {
|
||||||
|
|
||||||
if !verified {
|
|
||||||
tokio::fs::remove_file("server.jar").await?;
|
tokio::fs::remove_file("server.jar").await?;
|
||||||
return Err("sha1 verifaction failed for downloaded file".into());
|
return Err("sha1 verifaction failed for downloaded file".into());
|
||||||
}
|
}
|
||||||
|
|
@ -294,8 +297,6 @@ impl LoaderDownloader for FabricDownloader {
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tokio::fs::create_dir("mods").await?;
|
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
server: Server {
|
server: Server {
|
||||||
version: create.version.clone().unwrap(),
|
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 create;
|
||||||
pub mod install;
|
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
use serde::Serialize;
|
||||||
use terminal_size;
|
use terminal_size;
|
||||||
use terminal_size::{Width, terminal_size as get_terminal_size};
|
|
||||||
use textwrap::{Options, fill};
|
use textwrap::{Options, fill};
|
||||||
|
use terminal_size::{terminal_size as get_terminal_size, Width};
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{config::Config, search::SearchResult};
|
||||||
config::Config,
|
|
||||||
search::{SearchQueryParams, SearchResult},
|
#[derive(Serialize)]
|
||||||
};
|
struct SearchQueryParams {
|
||||||
|
query: String,
|
||||||
|
facets: String,
|
||||||
|
offset: Option<u32>,
|
||||||
|
limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Search {
|
pub struct Search {
|
||||||
query: String,
|
query: String,
|
||||||
|
|
@ -19,12 +25,12 @@ pub struct Search {
|
||||||
impl Search {
|
impl Search {
|
||||||
const BASE_URL: &str = "https://api.modrinth.com/v2";
|
const BASE_URL: &str = "https://api.modrinth.com/v2";
|
||||||
|
|
||||||
pub fn new(query: String, offset: Option<u32>, limit: Option<u32>) -> Self {
|
pub fn new(
|
||||||
Search {
|
query: String,
|
||||||
query,
|
offset: Option<u32>,
|
||||||
offset,
|
limit: Option<u32>,
|
||||||
limit,
|
) -> Self {
|
||||||
}
|
Search { query, offset, limit }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&self) -> Result<(), Box<dyn Error>> {
|
pub async fn run(&self) -> Result<(), Box<dyn Error>> {
|
||||||
|
|
@ -69,18 +75,12 @@ impl Search {
|
||||||
fn print_results(&self, res: &SearchResult) {
|
fn print_results(&self, res: &SearchResult) {
|
||||||
println!(
|
println!(
|
||||||
"{}\n",
|
"{}\n",
|
||||||
format!(
|
format!("Found {} result{}", res.total_hits, if res.total_hits == 1 { "" } else { "s" })
|
||||||
"Found {} result{}",
|
.green()
|
||||||
res.total_hits,
|
.bold()
|
||||||
if res.total_hits == 1 { "" } else { "s" }
|
|
||||||
)
|
|
||||||
.green()
|
|
||||||
.bold()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let width = get_terminal_size()
|
let width = get_terminal_size().map(|(Width(w), _)| w as usize).unwrap_or(80);
|
||||||
.map(|(Width(w), _)| w as usize)
|
|
||||||
.unwrap_or(80);
|
|
||||||
|
|
||||||
for (i, hit) in res.hits.iter().enumerate() {
|
for (i, hit) in res.hits.iter().enumerate() {
|
||||||
let wrapped = fill(
|
let wrapped = fill(
|
||||||
|
|
|
||||||
19
src/main.rs
19
src/main.rs
|
|
@ -2,7 +2,7 @@ mod commands;
|
||||||
mod models;
|
mod models;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use crate::commands::{create::Create, install::Install, search::Search};
|
use crate::commands::{create::Create, search::Search};
|
||||||
use clap::{
|
use clap::{
|
||||||
Parser, Subcommand,
|
Parser, Subcommand,
|
||||||
builder::styling::{self},
|
builder::styling::{self},
|
||||||
|
|
@ -68,13 +68,6 @@ enum Commands {
|
||||||
#[arg(long, value_parser = clap::value_parser!(u32).range(1..=100))]
|
#[arg(long, value_parser = clap::value_parser!(u32).range(1..=100))]
|
||||||
limit: Option<u32>,
|
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]
|
#[tokio::main]
|
||||||
|
|
@ -108,18 +101,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let config = create.download_server().await?;
|
let config = create.download_server().await?;
|
||||||
create.save_config(config).await?;
|
create.save_config(config).await?;
|
||||||
}
|
}
|
||||||
Commands::Search {
|
Commands::Search { query, offset, limit } => {
|
||||||
query,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
} => {
|
|
||||||
let search = Search::new(query, offset, limit);
|
let search = Search::new(query, offset, limit);
|
||||||
search.run().await?;
|
search.run().await?;
|
||||||
}
|
}
|
||||||
Commands::Install { mods } => {
|
|
||||||
let install = Install::new(mods);
|
|
||||||
install.run().await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 config;
|
||||||
pub mod fabric;
|
pub mod fabric;
|
||||||
pub mod install;
|
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod vanilla;
|
pub mod vanilla;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,4 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct SearchQueryParams {
|
|
||||||
pub query: String,
|
|
||||||
pub facets: String,
|
|
||||||
pub offset: Option<u32>,
|
|
||||||
pub limit: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct SearchResult {
|
pub struct SearchResult {
|
||||||
|
|
|
||||||
17
src/utils.rs
17
src/utils.rs
|
|
@ -1,6 +1,4 @@
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use sha1::{Digest, Sha1};
|
|
||||||
use sha2::Sha512;
|
|
||||||
use std::{error::Error, future::Future};
|
use std::{error::Error, future::Future};
|
||||||
|
|
||||||
pub async fn with_spinner<F, Fut, T>(
|
pub async fn with_spinner<F, Fut, T>(
|
||||||
|
|
@ -44,20 +42,7 @@ where
|
||||||
pb.set_message(start_msg.to_string());
|
pb.set_message(start_msg.to_string());
|
||||||
|
|
||||||
let result = f(pb.clone()).await;
|
let result = f(pb.clone()).await;
|
||||||
let formatted_finish_msg = format!("{} {}", dialoguer::console::style("✔").green(), finish_msg);
|
pb.finish_with_message(finish_msg.to_string());
|
||||||
pb.finish_with_message(formatted_finish_msg);
|
|
||||||
|
|
||||||
result
|
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