diff --git a/src/commands/create.rs b/src/commands/create.rs new file mode 100644 index 0000000..cfd2ebb --- /dev/null +++ b/src/commands/create.rs @@ -0,0 +1,248 @@ +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 new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/src/main.rs b/src/main.rs index 217e4cb..1225c63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,13 @@ +mod commands; mod models; mod utils; -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 crate::commands::create::Create; 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() @@ -42,189 +34,10 @@ async fn main() -> Result<(), Box> { match mcsm.command { Commands::Create { project_name } => { - 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()), - } + let mut create = Create::new(project_name.clone()).await?; + create.init_loader().await?; + let config = create.download_server().await?; + create.save_config(config).await?; } }