feat: vanilla server downloading + lots of ux improvements

This commit is contained in:
RafayAhmad7548 2026-08-30 18:29:17 +05:00
parent 505bfbbb5d
commit 0a12b87a2a
No known key found for this signature in database
6 changed files with 241 additions and 22 deletions

126
Cargo.lock generated
View file

@ -93,6 +93,15 @@ version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@ -217,6 +226,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -252,6 +267,15 @@ dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "dialoguer"
version = "0.12.0"
@ -265,6 +289,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.7"
@ -361,6 +396,23 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-io"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-macro"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "futures-sink"
version = "0.3.34"
@ -380,7 +432,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"slab",
]
@ -491,6 +547,15 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hybrid-array"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
dependencies = [
"typenum",
]
[[package]]
name = "hyper"
version = "1.11.1"
@ -666,6 +731,19 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "indicatif"
version = "0.18.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c"
dependencies = [
"console",
"portable-atomic",
"unicode-width",
"unit-prefix",
"web-time",
]
[[package]]
name = "ipnet"
version = "2.12.1"
@ -799,8 +877,11 @@ version = "0.1.0"
dependencies = [
"clap",
"dialoguer",
"futures-util",
"indicatif",
"reqwest",
"serde",
"sha1",
"tokio",
"toml",
]
@ -887,6 +968,12 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "potential_utf"
version = "0.1.6"
@ -1022,6 +1109,7 @@ dependencies = [
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
@ -1043,12 +1131,14 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
]
@ -1280,6 +1370,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shell-words"
version = "1.1.1"
@ -1649,6 +1750,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@ -1661,6 +1768,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unit-prefix"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -1771,6 +1884,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-streams"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.104"

View file

@ -6,7 +6,10 @@ edition = "2024"
[dependencies]
clap = { version = "4.6.6", features = ["derive"] }
dialoguer = { version = "0.12.0", features = ["fuzzy-select"] }
reqwest = { version = "0.13.4", features = ["json"] }
futures-util = "0.3.34"
indicatif = "0.18.6"
reqwest = { version = "0.13.4", features = ["json", "stream"] }
serde = { version = "1.0.229", features = ["derive"] }
sha1 = "0.11.0"
tokio = { version = "1.53.1", features = ["full"] }
toml = "1.1.4"

BIN
new/server.jar Normal file

Binary file not shown.

View file

@ -1,10 +1,17 @@
mod models;
mod utils;
use std::{error::Error, fs};
use clap::{Parser, Subcommand, builder::styling::{self}};
use crate::{models::vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions}, utils::with_spinner};
use clap::{
Parser, Subcommand,
builder::styling::{self},
};
use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme};
use crate::models::vanilla::{VanillaVersionType, VanillaVersions};
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use sha1::{Digest, Sha1};
use std::error::Error;
use tokio::io::AsyncWriteExt;
fn styles() -> styling::Styles {
styling::Styles::styled()
@ -22,17 +29,17 @@ struct Mcsm {
#[derive(Subcommand)]
enum Commands {
Init {
name: String
}
Init { name: String },
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let mcsm = Mcsm::parse();
match mcsm.command {
Commands::Init { name } => {
fs::create_dir(name)?;
Commands::Init { name: project_name } => {
tokio::fs::create_dir(&project_name).await?;
std::env::set_current_dir(&project_name)?;
let loaders = vec!["Vanilla", "Fabric"];
let loader_index = Select::with_theme(&ColorfulTheme::default())
@ -43,28 +50,71 @@ async fn main() -> Result<(), Box<dyn Error>> {
match loader_index {
0 => {
const URL: &str = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
const URL: &str =
"https://launchermeta.mojang.com/mc/game/version_manifest.json";
let res = with_spinner("", "", || async {
let res: VanillaVersions = reqwest::get(URL).await?.json().await?;
let release_versions = res.versions
Ok(res)
}).await?;
let release_versions = res
.versions
.iter()
.filter(|version| version.version_type == VanillaVersionType::Release)
.collect::<Vec<_>>();
let release_version_names = release_versions
.iter()
.map(|version| version.id.clone())
.collect::<Vec<_>>();
let version_index = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Select Version")
.items(&release_versions)
.items(&release_version_names)
.default(0)
.interact()?;
println!("you selected {} with version {}", loaders[loader_index], release_versions[version_index]);
let version_detail: VanillaVersionDetail =
reqwest::get(release_versions[version_index].url.clone())
.await?
.json()
.await?;
println!(
"downloading server.jar for {} {}",
loaders[loader_index], release_version_names[version_index]
);
let pb = ProgressBar::new(version_detail.downloads.server.size);
pb.set_style(ProgressStyle::default_bar().template(
"{bar:40.green/white} {bytes}/{total_bytes} ({bytes_per_sec})",
)?);
},
1 => {
let server_bytes = reqwest::get(version_detail.downloads.server.url).await?;
let mut file = tokio::fs::File::create("server.jar").await?;
let mut stream = server_bytes.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
pb.inc(chunk.len() as u64);
}
_ => return Err("Invalid selection".into())
pb.finish_with_message("download finished");
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();
if hex != version_detail.downloads.server.sha1 {
tokio::fs::remove_file("server.jar").await?;
return Err("sha1 verifaction failed for downloaded file".into());
}
Ok(())
}).await?;
}
1 => {}
_ => return Err("Invalid selection".into()),
}
}
}

View file

@ -29,3 +29,20 @@ pub struct VanillaVersion {
pub url: String,
}
#[derive(Deserialize)]
pub struct VanillaVersionDetail {
pub downloads: VanillaVersionDownloads,
}
#[derive(Deserialize)]
pub struct VanillaVersionDownloads {
pub client: VanillaVersionDownloadItem,
pub server: VanillaVersionDownloadItem,
}
#[derive(Deserialize)]
pub struct VanillaVersionDownloadItem {
pub sha1: String,
pub size: u64,
pub url: String,
}

23
src/utils.rs Normal file
View file

@ -0,0 +1,23 @@
use std::{error::Error, future::Future};
use indicatif::ProgressBar;
pub async fn with_spinner<F, Fut, T>(
start_msg: &str,
finish_msg: &str,
f: F,
) -> Result<T, Box<dyn Error>>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<T, Box<dyn Error>>>,
{
let pb = ProgressBar::new_spinner();
pb.set_message(start_msg.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let result = f().await;
pb.finish_and_clear();
println!("{} {}", dialoguer::console::style("✔").green(), finish_msg);
result
}