diff --git a/new/server.jar b/new/server.jar new file mode 100644 index 0000000..eeb02b2 Binary files /dev/null and b/new/server.jar differ diff --git a/src/commands/create.rs b/src/commands/create.rs deleted file mode 100644 index cfd2ebb..0000000 --- a/src/commands/create.rs +++ /dev/null @@ -1,248 +0,0 @@ -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::{ - models::{ - config::{Config, Mods, Server}, - fabric::FabricVersions, - vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions}, - }, - utils::{with_progressbar, with_spinner}, -}; - -pub struct Create { - // project_name: String, - loader: Option, - config_file: File, -} - -impl Create { - const LOADERS: [&str; 2] = ["Vanilla", "Fabric"]; - - pub async fn new(project_name: String) -> Result> { - tokio::fs::create_dir_all(&project_name).await?; - std::env::set_current_dir(&project_name)?; - let config_file = tokio::fs::File::create("mcsm.toml").await?; - Ok(Create { - /*project_name,*/ - loader: None, - config_file, - }) - } - - pub async fn init_loader(&mut self) -> Result<(), Box> { - let loader_index = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Select Loader") - .items(Self::LOADERS) - .default(0) - .interact()?; - self.loader = Some(Self::LOADERS[loader_index].to_string()); - Ok(()) - } - - pub async fn download_server(&self) -> Result> { - Ok(match self.loader.as_deref() { - Some("Vanilla") => VanillaDownloader::download().await?, - Some("Fabric") => FabricDownloader::download().await?, - _ => return Err("Invalid selection".into()), - }) - } - - pub async fn save_config(&mut self, config: Config) -> Result<(), Box> { - self.config_file - .write_all(toml::to_string(&config)?.as_bytes()) - .await?; - Ok(()) - } -} - -trait LoaderDownloader { - fn name() -> String; - async fn download() -> Result>; -} - -struct VanillaDownloader {} -impl LoaderDownloader for VanillaDownloader { - fn name() -> String { - "Vanilla".to_string() - } - - async fn download() -> Result> { - 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?; - Ok(res) - }) - .await?; - - let release_versions = res - .versions - .iter() - .filter(|version| version.version_type == VanillaVersionType::Release) - .collect::>(); - let release_version_names = release_versions - .iter() - .map(|version| version.id.clone()) - .collect::>(); - - let version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) - .with_prompt("Select Version") - .items(&release_version_names) - .default(0) - .interact()?; - - let version_detail: VanillaVersionDetail = with_spinner("", "", || async { - let version_detail: VanillaVersionDetail = - reqwest::get(release_versions[version_index].url.clone()) - .await? - .json() - .await?; - Ok(version_detail) - }) - .await?; - - let start_msg = format!( - "downloading server.jar for {} {}", - Self::name(), - release_version_names[version_index] - ); - with_progressbar( - &start_msg, - "download finished", - version_detail.downloads.server.size, - |pb| async move { - 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); - } - Ok(()) - }, - ) - .await?; - - 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?; - - let config = Config { - server: Server { - version: release_version_names[version_index].to_string(), - loader: Self::name(), - loader_version: None, - }, - mods: Mods {}, - }; - - Ok(config) - } -} - -struct FabricDownloader {} -impl LoaderDownloader for FabricDownloader { - fn name() -> String { - "Fabric".to_string() - } - - async fn download() -> Result> { - const URL: &str = "https://meta.fabricmc.net/v2/versions"; - let res = with_spinner("", "", || async { - let res: FabricVersions = reqwest::get(URL).await?.json().await?; - Ok(res) - }) - .await?; - - let game_version_names = res - .game - .iter() - .filter(|game| game.stable) - .map(|game| game.version.clone()) - .collect::>(); - - let game_version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) - .with_prompt("Select Game Version") - .items(&game_version_names) - .default(0) - .interact()?; - - let loader_version_names = res - .loader - .iter() - .filter(|loader| loader.stable) - .map(|loader| loader.version.clone()) - .collect::>(); - - let loader_version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) - .with_prompt("Select Loader Version") - .items(&loader_version_names) - .default(0) - .interact()?; - - let game_version = game_version_names[game_version_index].clone(); - let loader_version = loader_version_names[loader_version_index].clone(); - let installer_version = res - .installer - .iter() - .find_map(|installer| { - if installer.stable { - Some(installer.version.clone()) - } else { - None - } - }) - .ok_or("No Installer Version Found")?; - - let start_msg = format!( - "downloading server.jar for {} {}, {}", - Self::name(), - loader_version_names[loader_version_index], - game_version_names[game_version_index], - ); - with_spinner(&start_msg, "download finished", || async { - let download_url = format!( - "{}/loader/{}/{}/{}/server/jar", - URL, game_version, loader_version, installer_version - ); - let res = reqwest::get(download_url).await?; - let mut file = tokio::fs::File::create("server.jar").await?; - let mut stream = res.bytes_stream(); - - while let Some(chunk) = stream.next().await { - file.write_all(&chunk?).await?; - } - - Ok(()) - }) - .await?; - - let config = Config { - server: Server { - version: game_version, - loader: Self::name(), - loader_version: Some(loader_version), - }, - mods: Mods {}, - }; - - Ok(config) - } -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs deleted file mode 100644 index c5fb369..0000000 --- a/src/commands/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod create; diff --git a/src/main.rs b/src/main.rs index 1225c63..217e4cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,21 @@ -mod commands; mod models; mod utils; -use crate::commands::create::Create; +use crate::models::config::{Config, Mods, Server}; +use crate::models::fabric::FabricVersions; +use crate::{ + models::vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions}, + utils::{with_progressbar, with_spinner}, +}; use clap::{ Parser, Subcommand, builder::styling::{self}, }; +use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme}; +use futures_util::StreamExt; +use sha1::{Digest, Sha1}; use std::error::Error; +use tokio::io::AsyncWriteExt; fn styles() -> styling::Styles { styling::Styles::styled() @@ -34,10 +42,189 @@ async fn main() -> Result<(), Box> { match mcsm.command { Commands::Create { project_name } => { - let mut create = Create::new(project_name.clone()).await?; - create.init_loader().await?; - let config = create.download_server().await?; - create.save_config(config).await?; + tokio::fs::create_dir_all(&project_name).await?; + std::env::set_current_dir(&project_name)?; + let mut config_file = tokio::fs::File::create("mcsm.toml").await?; + + let loaders = vec!["Vanilla", "Fabric"]; + let loader_index = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Select Loader") + .items(&loaders) + .default(0) + .interact()?; + + match loader_index { + 0 => { + 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?; + Ok(res) + }) + .await?; + + let release_versions = res + .versions + .iter() + .filter(|version| version.version_type == VanillaVersionType::Release) + .collect::>(); + let release_version_names = release_versions + .iter() + .map(|version| version.id.clone()) + .collect::>(); + + let version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select Version") + .items(&release_version_names) + .default(0) + .interact()?; + + let version_detail: VanillaVersionDetail = with_spinner("", "", || async { + let version_detail: VanillaVersionDetail = + reqwest::get(release_versions[version_index].url.clone()) + .await? + .json() + .await?; + Ok(version_detail) + }) + .await?; + + let start_msg = format!( + "downloading server.jar for {} {}", + loaders[loader_index], release_version_names[version_index] + ); + with_progressbar( + &start_msg, + "download finished", + version_detail.downloads.server.size, + |pb| async move { + 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); + } + Ok(()) + }, + ) + .await?; + + 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?; + + let config = Config { + server: Server { + version: release_version_names[version_index].to_string(), + loader: loaders[loader_index].to_string(), + loader_version: None, + }, + mods: Mods {}, + }; + config_file + .write_all(toml::to_string(&config)?.as_bytes()) + .await?; + } + 1 => { + const URL: &str = "https://meta.fabricmc.net/v2/versions"; + let res = with_spinner("", "", || async { + let res: FabricVersions = reqwest::get(URL).await?.json().await?; + Ok(res) + }) + .await?; + + let game_version_names = res + .game + .iter() + .filter(|game| game.stable) + .map(|game| game.version.clone()) + .collect::>(); + + let game_version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select Game Version") + .items(&game_version_names) + .default(0) + .interact()?; + + let loader_version_names = res + .loader + .iter() + .filter(|loader| loader.stable) + .map(|loader| loader.version.clone()) + .collect::>(); + + let loader_version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select Loader Version") + .items(&loader_version_names) + .default(0) + .interact()?; + + let game_version = game_version_names[game_version_index].clone(); + let loader_version = loader_version_names[loader_version_index].clone(); + let installer_version = res + .installer + .iter() + .find_map(|installer| { + if installer.stable { + Some(installer.version.clone()) + } else { + None + } + }) + .ok_or("No Installer Version Found")?; + + let start_msg = format!( + "downloading server.jar for {} {}, {}", + loaders[loader_index], + loader_version_names[loader_version_index], + game_version_names[game_version_index], + ); + with_spinner(&start_msg, "download finished", || async { + let download_url = format!( + "{}/loader/{}/{}/{}/server/jar", + URL, game_version, loader_version, installer_version + ); + let res = reqwest::get(download_url).await?; + let mut file = tokio::fs::File::create("server.jar").await?; + let mut stream = res.bytes_stream(); + + while let Some(chunk) = stream.next().await { + file.write_all(&chunk?).await?; + } + + Ok(()) + }) + .await?; + + let config = Config { + server: Server { + version: game_version, + loader: loaders[loader_index].to_string(), + loader_version: Some(loader_version), + }, + mods: Mods {}, + }; + config_file + .write_all(toml::to_string(&config)?.as_bytes()) + .await?; + } + _ => return Err("Invalid selection".into()), + } } }