From 4acaf86882f02231aada4338b5fe09bd296bf735 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Sat, 8 Jul 2023 15:39:21 +0300 Subject: [PATCH 1/6] *casually adds fabric and quilt* --- Cargo.lock | 19 +++- Cargo.toml | 4 +- src/commands/build.rs | 171 ++++++++++++++++++++++++------ src/commands/init.rs | 9 +- src/downloadable/interactive.rs | 2 +- src/downloadable/mod.rs | 8 +- src/downloadable/sources/quilt.rs | 38 +++++++ src/model/server.rs | 2 +- 8 files changed, 208 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5369386..1a5ba1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,12 +753,12 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "mcapi" -version = "0.1.0" -source = "git+https://github.com/ParadigmMC/mcapi.git#9f30bf384d12314943c57ebd6b521b2e7131e6a1" +version = "0.2.0" dependencies = [ "os-version", "regex", "reqwest", + "roxmltree", "serde", "serde_json", "thiserror", @@ -1043,6 +1043,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "roxmltree" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8" +dependencies = [ + "xmlparser", +] + [[package]] name = "rustix" version = "0.37.19" @@ -1833,6 +1842,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "xmlparser" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index ba6bec0..95198e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ futures = "0.3" indexmap = "2.0.0" indicatif = "0.17" java-properties = { git = "https://github.com/ParadigmMC/java-properties.git" } -mcapi = { git = "https://github.com/ParadigmMC/mcapi.git" } -#mcapi = { path = "../mcapi" } +#mcapi = { git = "https://github.com/ParadigmMC/mcapi.git" } +mcapi = { path = "../mcapi" } pathdiff = { git = "https://github.com/Manishearth/pathdiff.git" } regex = "1.8" reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"], default-features = false } diff --git a/src/commands/build.rs b/src/commands/build.rs index 726576b..7bb8071 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -2,14 +2,15 @@ use std::{ collections::HashMap, env, fs::{self, OpenOptions}, - io::Write, + io::{Write, BufReader, BufRead}, path::{Path, PathBuf}, - time::Instant, + time::{Instant, Duration}, process::Stdio }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use clap::{arg, value_parser, ArgMatches, Command}; use console::{style, Style}; +use indicatif::{ProgressBar, ProgressStyle}; use tokio::fs::File; use super::version::APP_USER_AGENT; @@ -101,6 +102,18 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { "config", )?; + if server.launcher.eula_args { + match server.jar { + Downloadable::Quilt { .. } + | Downloadable::Fabric { .. } => { + println!(" {}", style("=> eula.txt [eula_args unsupported]").dim()); + std::fs::File::create(output_dir.join("eula.txt"))? + .write_all(b"eula=true")?; + } + _ => (), + } + } + println!(" {}", style("Bootstrapping complete").dim()); // stage 5: launcher scripts @@ -159,38 +172,134 @@ async fn download_server_jar( http_client: &reqwest::Client, output_dir: &Path, ) -> Result { - let serverjar_name = server.jar.get_filename(server, http_client).await?; - if output_dir.join(serverjar_name.clone()).exists() { - println!( - " Skipping server jar ({})", - style(serverjar_name.clone()).dim() - ); - } else { - println!( - " Downloading server jar ({})", - style(serverjar_name.clone()).dim() - ); + let serverjar_name = match &server.jar { + Downloadable::Quilt { loader, .. } => { + let installerjar_name = server.jar.get_filename(server, http_client).await?; + if output_dir.join(installerjar_name.clone()).exists() { + println!( + " Quilt installer present ({})", + style(installerjar_name.clone()).dim() + ); + } else { + println!( + " Downloading quilt installer... ({})", + style(installerjar_name.clone()).dim() + ); + + let filename = &server.jar.get_filename(server, http_client).await?; + util::download_with_progress( + File::create(&output_dir.join(filename)) + .await + .context(format!("Failed to create output file for {filename}"))?, + filename, + &server.jar, + server, + http_client, + ) + .await?; + } - let filename = &server.jar.get_filename(server, http_client).await?; - util::download_with_progress( - File::create(&output_dir.join(filename)) - .await - .context(format!("Failed to create output file for {filename}"))?, - filename, - &server.jar, - server, - http_client, - ) - .await?; + let serverjar_name = format!("quilt-server-launch-{}-{}.jar", server.mc_version, loader); + + if output_dir.join(serverjar_name.clone()).exists() { + println!( + " Skipping server jar ({})", + style(serverjar_name.clone()).dim() + ); + } else { + println!( + " Installing quilt server... ({})", + style(serverjar_name.clone()).dim() + ); + + let mut args = vec![ + "-jar", + &installerjar_name, + "install", + "server", + &server.mc_version, + ]; + + if loader != "latest" { + args.push(&loader); + } + + args.push("--install-dir=."); + args.push("--download-server"); + + let mut child = std::process::Command::new("java") + .args(args) + .current_dir(output_dir) + .stdout(Stdio::piped()) + .spawn() + .context("Running quilt-server-installer")?; + + let spinner = ProgressBar::new_spinner() + .with_style( + ProgressStyle::with_template(" {spinner:.dim.bold} {msg}")? + ); + + spinner.enable_steady_tick(Duration::from_millis(200)); + + let prefix = style("[qsi]").bold(); + + for line in BufReader::new(child.stdout.take().unwrap()).lines() { + let line = line.unwrap(); + let stripped_line = line.trim(); + if !stripped_line.is_empty() { + spinner.set_message(format!("{prefix} {stripped_line}")); + } + } + + if !child.wait()?.success() { + bail!("Quilt server installer exited with non-zero code"); + } + + spinner.finish_and_clear(); + + println!( + " Renaming... ({})", + style("quilt-server-launch.jar => ".to_owned() + &serverjar_name).dim() + ); + + fs::rename( + output_dir.join("quilt-server-launch.jar"), + output_dir.join(&serverjar_name), + ).context("Renaming quilt-server-launch.jar")?; + } - match &server.jar { - Downloadable::Quilt { .. } | Downloadable::Fabric { .. } => { - todo!() + serverjar_name + }, + dl => { + let serverjar_name = dl.get_filename(server, http_client).await?; + if output_dir.join(serverjar_name.clone()).exists() { + println!( + " Skipping server jar ({})", + style(serverjar_name.clone()).dim() + ); + } else { + println!( + " Downloading server jar ({})", + style(serverjar_name.clone()).dim() + ); + + let filename = &dl.get_filename(server, http_client).await?; + util::download_with_progress( + File::create(&output_dir.join(filename)) + .await + .context(format!("Failed to create output file for {filename}"))?, + filename, + &dl, + server, + http_client, + ) + .await?; } - _ => (), + + serverjar_name } - } - + }; + Ok(serverjar_name) } diff --git a/src/commands/init.rs b/src/commands/init.rs index 2ac8259..4b92e83 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -3,7 +3,8 @@ use dialoguer::Confirm; use dialoguer::{theme::ColorfulTheme, Input, Select}; use std::fs::File; use std::io::Write; -use std::{ffi::OsStr, path::PathBuf}; +use std::path::Path; +use std::ffi::OsStr; use crate::commands::markdown; use crate::{ @@ -108,10 +109,10 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { ..Default::default() }; - initialize_environment(is_proxy)?; + initialize_environment(is_proxy).context("Initializing environment")?; server.save()?; - let write_readme = if PathBuf::from("./README.md").exists() { + let write_readme = if Path::new("./README.md").exists() { Confirm::with_theme(&theme) .default(true) .with_prompt("Overwrite README.md?") @@ -121,7 +122,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { }; if write_readme { - markdown::initialize_readme(&server)?; + markdown::initialize_readme(&server).context("Initializing readme")?; } println!(" > {}", style("Server has been initialized!").cyan()); diff --git a/src/downloadable/interactive.rs b/src/downloadable/interactive.rs index 4f9dabe..3e0b5f8 100644 --- a/src/downloadable/interactive.rs +++ b/src/downloadable/interactive.rs @@ -39,7 +39,7 @@ impl Downloadable { let items_str: Vec = items.iter().map(|v| v.1.to_owned()).collect(); let jar_type = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which server software to use?") + .with_prompt("Which (modded) server software to use?") .default(0) .items(&items_str) .interact()?; diff --git a/src/downloadable/mod.rs b/src/downloadable/mod.rs index 3c3aeb6..4e8aa56 100644 --- a/src/downloadable/mod.rs +++ b/src/downloadable/mod.rs @@ -13,7 +13,7 @@ use self::sources::{ modrinth::{download_modrinth, fetch_modrinth_filename}, papermc::{download_papermc_build, fetch_papermc_build}, purpur::{download_purpurmc_build, fetch_purpurmc_builds}, - quilt::{download_quilt_installer, get_quilt_filename}, + quilt::{download_quilt_installer, get_installer_filename}, spigot::{download_spigot_resource, fetch_spigot_resource_latest_ver}, vanilla::fetch_vanilla, }; @@ -169,8 +169,8 @@ impl Downloadable { Ok(download_fabric(client, &mcver, loader, installer).await?) } - Self::Quilt { loader, installer } => { - Ok(download_quilt_installer(client, &mcver, loader, installer).await?) + Self::Quilt { installer, .. } => { + Ok(download_quilt_installer(client, installer).await?) } } } @@ -266,7 +266,7 @@ impl Downloadable { )) } - Self::Quilt { loader, .. } => Ok(get_quilt_filename(client, &mcver, loader).await?), + Self::Quilt { installer, .. } => Ok(get_installer_filename(client, installer).await?), } } } diff --git a/src/downloadable/sources/quilt.rs b/src/downloadable/sources/quilt.rs index 2737b38..056a320 100644 --- a/src/downloadable/sources/quilt.rs +++ b/src/downloadable/sources/quilt.rs @@ -2,6 +2,43 @@ #![allow(unused)] use anyhow::{anyhow, Result}; +use mcapi::quilt::{InstallerVariant, self}; + +pub async fn download_quilt_installer( + client: &reqwest::Client, + installer: &str, +) -> Result { + let v = match installer { + "latest" => fetch_latest_quilt_installer(client).await?, + id => id.to_owned(), + }; + + Ok(quilt::download_installer(client, &InstallerVariant::Universal, &v).await?) +} + +pub async fn fetch_latest_quilt_installer( + client: &reqwest::Client +) -> Result { + Ok( + mcapi::quilt::fetch_installer_versions(client, &InstallerVariant::Universal).await? + .last().expect("latest quilt installer version to be present").clone() + ) +} + +pub async fn get_installer_filename( + client: &reqwest::Client, + installer: &str, +) -> Result { + let v = match installer { + "latest" => fetch_latest_quilt_installer(client).await?, + id => id.to_owned(), + }; + + Ok(format!("quilt-installer-{v}.jar")) +} + + +/* pub async fn fetch_quilt_latest_loader(client: &reqwest::Client) -> Result { let loaders = mcapi::quilt::fetch_loaders(client).await?; @@ -53,3 +90,4 @@ pub async fn get_quilt_filename( Ok(format!("quilt-server-{mcver}-{l}-launch.jar")) } + */ \ No newline at end of file diff --git a/src/model/server.rs b/src/model/server.rs index 636d659..b62011f 100644 --- a/src/model/server.rs +++ b/src/model/server.rs @@ -201,7 +201,7 @@ impl Default for Server { let mut vars = HashMap::new(); vars.insert("PORT".to_owned(), "25565".to_owned()); Self { - path: PathBuf::from("./server.toml"), + path: PathBuf::from("."), name: String::new(), mc_version: "latest".to_owned(), jar: Downloadable::Vanilla {}, From 0fe504c3eba0d131f15574b7e80b91c23a4231a4 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Sat, 8 Jul 2023 15:39:21 +0300 Subject: [PATCH 2/6] *casually adds fabric and quilt* --- Cargo.lock | 19 +++- Cargo.toml | 4 +- src/commands/build.rs | 171 ++++++++++++++++++++++++------ src/commands/import/mrpack.rs | 111 +++++++++++++++++++ src/commands/init.rs | 9 +- src/downloadable/interactive.rs | 2 +- src/downloadable/mod.rs | 8 +- src/downloadable/sources/quilt.rs | 38 +++++++ src/model/server.rs | 2 +- src/util/mrpack.rs | 128 ++++++++++++++++++++++ 10 files changed, 447 insertions(+), 45 deletions(-) create mode 100644 src/commands/import/mrpack.rs create mode 100644 src/util/mrpack.rs diff --git a/Cargo.lock b/Cargo.lock index 5369386..1a5ba1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,12 +753,12 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "mcapi" -version = "0.1.0" -source = "git+https://github.com/ParadigmMC/mcapi.git#9f30bf384d12314943c57ebd6b521b2e7131e6a1" +version = "0.2.0" dependencies = [ "os-version", "regex", "reqwest", + "roxmltree", "serde", "serde_json", "thiserror", @@ -1043,6 +1043,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "roxmltree" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8" +dependencies = [ + "xmlparser", +] + [[package]] name = "rustix" version = "0.37.19" @@ -1833,6 +1842,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "xmlparser" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index ba6bec0..95198e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ futures = "0.3" indexmap = "2.0.0" indicatif = "0.17" java-properties = { git = "https://github.com/ParadigmMC/java-properties.git" } -mcapi = { git = "https://github.com/ParadigmMC/mcapi.git" } -#mcapi = { path = "../mcapi" } +#mcapi = { git = "https://github.com/ParadigmMC/mcapi.git" } +mcapi = { path = "../mcapi" } pathdiff = { git = "https://github.com/Manishearth/pathdiff.git" } regex = "1.8" reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"], default-features = false } diff --git a/src/commands/build.rs b/src/commands/build.rs index bb6074b..7b82358 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -2,14 +2,15 @@ use std::{ collections::HashMap, env, fs::{self, OpenOptions}, - io::Write, + io::{Write, BufReader, BufRead}, path::{Path, PathBuf}, - time::Instant, + time::{Instant, Duration}, process::Stdio }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use clap::{arg, value_parser, ArgMatches, Command}; use console::{style, Style}; +use indicatif::{ProgressBar, ProgressStyle}; use tokio::fs::File; use super::version::APP_USER_AGENT; @@ -120,6 +121,18 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { "config", )?; + if server.launcher.eula_args { + match server.jar { + Downloadable::Quilt { .. } + | Downloadable::Fabric { .. } => { + println!(" {}", style("=> eula.txt [eula_args unsupported]").dim()); + std::fs::File::create(output_dir.join("eula.txt"))? + .write_all(b"eula=true")?; + } + _ => (), + } + } + println!(" {}", style("Bootstrapping complete").dim()); } else { mark_stage_skipped("bootstrap"); @@ -149,38 +162,134 @@ async fn download_server_jar( http_client: &reqwest::Client, output_dir: &Path, ) -> Result { - let serverjar_name = server.jar.get_filename(server, http_client).await?; - if output_dir.join(serverjar_name.clone()).exists() { - println!( - " Skipping server jar ({})", - style(serverjar_name.clone()).dim() - ); - } else { - println!( - " Downloading server jar ({})", - style(serverjar_name.clone()).dim() - ); + let serverjar_name = match &server.jar { + Downloadable::Quilt { loader, .. } => { + let installerjar_name = server.jar.get_filename(server, http_client).await?; + if output_dir.join(installerjar_name.clone()).exists() { + println!( + " Quilt installer present ({})", + style(installerjar_name.clone()).dim() + ); + } else { + println!( + " Downloading quilt installer... ({})", + style(installerjar_name.clone()).dim() + ); + + let filename = &server.jar.get_filename(server, http_client).await?; + util::download_with_progress( + File::create(&output_dir.join(filename)) + .await + .context(format!("Failed to create output file for {filename}"))?, + filename, + &server.jar, + server, + http_client, + ) + .await?; + } - let filename = &server.jar.get_filename(server, http_client).await?; - util::download_with_progress( - File::create(&output_dir.join(filename)) - .await - .context(format!("Failed to create output file for {filename}"))?, - filename, - &server.jar, - server, - http_client, - ) - .await?; + let serverjar_name = format!("quilt-server-launch-{}-{}.jar", server.mc_version, loader); + + if output_dir.join(serverjar_name.clone()).exists() { + println!( + " Skipping server jar ({})", + style(serverjar_name.clone()).dim() + ); + } else { + println!( + " Installing quilt server... ({})", + style(serverjar_name.clone()).dim() + ); + + let mut args = vec![ + "-jar", + &installerjar_name, + "install", + "server", + &server.mc_version, + ]; + + if loader != "latest" { + args.push(&loader); + } + + args.push("--install-dir=."); + args.push("--download-server"); + + let mut child = std::process::Command::new("java") + .args(args) + .current_dir(output_dir) + .stdout(Stdio::piped()) + .spawn() + .context("Running quilt-server-installer")?; + + let spinner = ProgressBar::new_spinner() + .with_style( + ProgressStyle::with_template(" {spinner:.dim.bold} {msg}")? + ); + + spinner.enable_steady_tick(Duration::from_millis(200)); + + let prefix = style("[qsi]").bold(); + + for line in BufReader::new(child.stdout.take().unwrap()).lines() { + let line = line.unwrap(); + let stripped_line = line.trim(); + if !stripped_line.is_empty() { + spinner.set_message(format!("{prefix} {stripped_line}")); + } + } + + if !child.wait()?.success() { + bail!("Quilt server installer exited with non-zero code"); + } + + spinner.finish_and_clear(); + + println!( + " Renaming... ({})", + style("quilt-server-launch.jar => ".to_owned() + &serverjar_name).dim() + ); + + fs::rename( + output_dir.join("quilt-server-launch.jar"), + output_dir.join(&serverjar_name), + ).context("Renaming quilt-server-launch.jar")?; + } - match &server.jar { - Downloadable::Quilt { .. } | Downloadable::Fabric { .. } => { - todo!() + serverjar_name + }, + dl => { + let serverjar_name = dl.get_filename(server, http_client).await?; + if output_dir.join(serverjar_name.clone()).exists() { + println!( + " Skipping server jar ({})", + style(serverjar_name.clone()).dim() + ); + } else { + println!( + " Downloading server jar ({})", + style(serverjar_name.clone()).dim() + ); + + let filename = &dl.get_filename(server, http_client).await?; + util::download_with_progress( + File::create(&output_dir.join(filename)) + .await + .context(format!("Failed to create output file for {filename}"))?, + filename, + &dl, + server, + http_client, + ) + .await?; } - _ => (), + + serverjar_name } - } - + }; + Ok(serverjar_name) } diff --git a/src/commands/import/mrpack.rs b/src/commands/import/mrpack.rs new file mode 100644 index 0000000..3b97a99 --- /dev/null +++ b/src/commands/import/mrpack.rs @@ -0,0 +1,111 @@ +use std::{path::PathBuf, fs::File}; + +use anyhow::{Context, Result, anyhow, bail}; +use clap::{arg, ArgMatches, Command}; +use console::style; +use dialoguer::{Select, theme::ColorfulTheme}; +use reqwest::Url; +use tempfile::Builder; + +use crate::{commands::version::APP_USER_AGENT, downloadable::{Downloadable, sources::modrinth::{fetch_modrinth_versions, ModrinthVersion}}, model::Server, util::{mrpack::import_from_mrpack, download_with_progress}}; + +pub fn cli() -> Command { + Command::new("mrpack") + .about("Import from .mrpack (modrinth modpacks)") + .arg(arg!( "File or url").required(true)) +} + +pub async fn run(matches: &ArgMatches) -> Result<()> { + let mut server = Server::load().context("Failed to load server.toml")?; + + let http_client = reqwest::Client::builder() + .user_agent(APP_USER_AGENT) + .build() + .context("Failed to create HTTP client")?; + + let src = matches.get_one::("source").unwrap(); + + let tmp_dir = Builder::new().prefix("mcman-mrpack-import").tempdir()?; + + let filename = if src.starts_with("http") { + println!(" > {}", style("Downloading mrpack...").green()); + + let fname = tmp_dir.path().join("pack.mrpack"); + let file = tokio::fs::File::create(&fname).await?; + + let url = Url::parse(src)?; + + let modpack_id = { + if url.domain() == Some("modrinth.com") + && url.path().starts_with("/modpack") { + url.path_segments() + .ok_or(anyhow!("Invalid modrinth /modpack URL"))? + .nth(1) + } else { + None + } + }; + + let downloadable = if let Some(id) = modpack_id { + let versions: Vec = fetch_modrinth_versions(&http_client, &id, None) + .await? + .into_iter() + .filter(|v| v.game_versions.contains(&server.mc_version)) + .collect(); + + let version = { + if versions.is_empty() { + bail!("No compatible versions in modrinth project"); + } + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(" Which version?") + .default(0) + .items( + &versions + .iter() + .map(|v| { + let num = &v.version_number; + let name = &v.name; + let compat = v.loaders.join(","); + format!("[{num}] {name} / {compat}") + }) + .collect::>(), + ) + .interact() + .unwrap(); + + versions[selection].clone() + }; + + Downloadable::Modrinth { + id: id.to_owned(), + version: version.id, + } + } else { + Downloadable::Url { url: src.clone(), filename: None, desc: None } + }; + + download_with_progress( + file, + &format!("Downloading {src}..."), + &downloadable, + &server, + &http_client, + ).await?; + + fname + } else { + PathBuf::from(src) + }; + + let f = File::open(filename).context("opening file")?; + + import_from_mrpack(&mut server, &http_client, f).await?; + + server.save()?; + + println!(" > Imported!"); + + Ok(()) +} diff --git a/src/commands/init.rs b/src/commands/init.rs index 2ac8259..4b92e83 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -3,7 +3,8 @@ use dialoguer::Confirm; use dialoguer::{theme::ColorfulTheme, Input, Select}; use std::fs::File; use std::io::Write; -use std::{ffi::OsStr, path::PathBuf}; +use std::path::Path; +use std::ffi::OsStr; use crate::commands::markdown; use crate::{ @@ -108,10 +109,10 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { ..Default::default() }; - initialize_environment(is_proxy)?; + initialize_environment(is_proxy).context("Initializing environment")?; server.save()?; - let write_readme = if PathBuf::from("./README.md").exists() { + let write_readme = if Path::new("./README.md").exists() { Confirm::with_theme(&theme) .default(true) .with_prompt("Overwrite README.md?") @@ -121,7 +122,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { }; if write_readme { - markdown::initialize_readme(&server)?; + markdown::initialize_readme(&server).context("Initializing readme")?; } println!(" > {}", style("Server has been initialized!").cyan()); diff --git a/src/downloadable/interactive.rs b/src/downloadable/interactive.rs index 4f9dabe..3e0b5f8 100644 --- a/src/downloadable/interactive.rs +++ b/src/downloadable/interactive.rs @@ -39,7 +39,7 @@ impl Downloadable { let items_str: Vec = items.iter().map(|v| v.1.to_owned()).collect(); let jar_type = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which server software to use?") + .with_prompt("Which (modded) server software to use?") .default(0) .items(&items_str) .interact()?; diff --git a/src/downloadable/mod.rs b/src/downloadable/mod.rs index 3c3aeb6..4e8aa56 100644 --- a/src/downloadable/mod.rs +++ b/src/downloadable/mod.rs @@ -13,7 +13,7 @@ use self::sources::{ modrinth::{download_modrinth, fetch_modrinth_filename}, papermc::{download_papermc_build, fetch_papermc_build}, purpur::{download_purpurmc_build, fetch_purpurmc_builds}, - quilt::{download_quilt_installer, get_quilt_filename}, + quilt::{download_quilt_installer, get_installer_filename}, spigot::{download_spigot_resource, fetch_spigot_resource_latest_ver}, vanilla::fetch_vanilla, }; @@ -169,8 +169,8 @@ impl Downloadable { Ok(download_fabric(client, &mcver, loader, installer).await?) } - Self::Quilt { loader, installer } => { - Ok(download_quilt_installer(client, &mcver, loader, installer).await?) + Self::Quilt { installer, .. } => { + Ok(download_quilt_installer(client, installer).await?) } } } @@ -266,7 +266,7 @@ impl Downloadable { )) } - Self::Quilt { loader, .. } => Ok(get_quilt_filename(client, &mcver, loader).await?), + Self::Quilt { installer, .. } => Ok(get_installer_filename(client, installer).await?), } } } diff --git a/src/downloadable/sources/quilt.rs b/src/downloadable/sources/quilt.rs index 2737b38..056a320 100644 --- a/src/downloadable/sources/quilt.rs +++ b/src/downloadable/sources/quilt.rs @@ -2,6 +2,43 @@ #![allow(unused)] use anyhow::{anyhow, Result}; +use mcapi::quilt::{InstallerVariant, self}; + +pub async fn download_quilt_installer( + client: &reqwest::Client, + installer: &str, +) -> Result { + let v = match installer { + "latest" => fetch_latest_quilt_installer(client).await?, + id => id.to_owned(), + }; + + Ok(quilt::download_installer(client, &InstallerVariant::Universal, &v).await?) +} + +pub async fn fetch_latest_quilt_installer( + client: &reqwest::Client +) -> Result { + Ok( + mcapi::quilt::fetch_installer_versions(client, &InstallerVariant::Universal).await? + .last().expect("latest quilt installer version to be present").clone() + ) +} + +pub async fn get_installer_filename( + client: &reqwest::Client, + installer: &str, +) -> Result { + let v = match installer { + "latest" => fetch_latest_quilt_installer(client).await?, + id => id.to_owned(), + }; + + Ok(format!("quilt-installer-{v}.jar")) +} + + +/* pub async fn fetch_quilt_latest_loader(client: &reqwest::Client) -> Result { let loaders = mcapi::quilt::fetch_loaders(client).await?; @@ -53,3 +90,4 @@ pub async fn get_quilt_filename( Ok(format!("quilt-server-{mcver}-{l}-launch.jar")) } + */ \ No newline at end of file diff --git a/src/model/server.rs b/src/model/server.rs index c43d3c5..d764cec 100644 --- a/src/model/server.rs +++ b/src/model/server.rs @@ -201,7 +201,7 @@ impl Default for Server { let mut vars = HashMap::new(); vars.insert("PORT".to_owned(), "25565".to_owned()); Self { - path: PathBuf::from("./server.toml"), + path: PathBuf::from("."), name: String::new(), mc_version: "latest".to_owned(), jar: Downloadable::Vanilla {}, diff --git a/src/util/mrpack.rs b/src/util/mrpack.rs new file mode 100644 index 0000000..819919c --- /dev/null +++ b/src/util/mrpack.rs @@ -0,0 +1,128 @@ +use anyhow::{Context, Result}; +use console::style; +use serde::{Deserialize, Serialize}; +use std::{io::{Read, Seek}, path::PathBuf, fs}; +use zip::ZipArchive; + +use crate::{model::Server, downloadable::Downloadable}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct MRPackIndex { + pub name: String, + pub files: Vec, +} + +impl MRPackIndex { + pub async fn import_all( + &self, + server: &mut Server, + http_client: &reqwest::Client, + ) -> Result<()> { + for f in self.files.iter().filter(|f| f.env.is_none() || f.env.as_ref().unwrap().server != EnvSupport::Unsupported) { + let url = f.downloads.first().context("unwrap url from downloads")?; + + let dl = Downloadable::from_url_interactive(http_client, server, url) + .await?; + + server.mods.push(dl); + } + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MRPackFile { + path: String, + env: Option, + downloads: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Env { + pub client: EnvSupport, + pub server: EnvSupport, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub enum EnvSupport { + Required, + Optional, + Unsupported, +} + +pub async fn import_from_mrpack( + server: &mut Server, + http_client: &reqwest::Client, + reader: R, +) -> Result<()> { + println!( + " > {}", + style("Importing mrpack...").cyan(), + ); + + let mut archive = ZipArchive::new(reader).context("reading mrpack zip archive")?; + + println!(" > {}", style("Reading index...").cyan()); + + let mut mr_index = archive.by_name("modrinth.index.json")?; + let mut zip_content = Vec::new(); + mr_index + .read_to_end(&mut zip_content) + .context("reading modrinth.index.json from zip file")?; + + + let pack: MRPackIndex = serde_json::from_slice(&zip_content)?; + pack.import_all(server, http_client) + .await + .context("importing from mrpack index")?; + + drop(mr_index); + + println!(" > {}", style("Extracting overrides...").cyan()); + + let mut server_overrided = vec![]; + let mut queue = vec![]; + + for filename in archive.file_names() { + if filename.ends_with('/') { + continue; // folder + } + + let path = PathBuf::from(filename); + + let real_path = if path.starts_with("overrides") { + if server_overrided.contains(&path) { + continue; + } + + server.path.join("config").join(path.strip_prefix("overrides")?) + } else { + if path.starts_with("server-overrides") { + server_overrided.push(path.clone()); + server.path.join("config").join(path.strip_prefix("server-overrides")?) + } else { + continue; + } + }; + + queue.push((path, real_path)); + } + + for (path, real_path) in queue { + let mut zip_file = archive.by_name(&path.to_string_lossy())?; + let mut zip_content = Vec::new(); + zip_file + .read_to_end(&mut zip_content) + .context(format!("reading {} from zip file", path.display()))?; + + fs::create_dir_all(real_path.parent().unwrap()) + .context(format!("Creating parent folder for {}", real_path.display()))?; + + fs::write(&real_path, zip_content) + .context(format!("Writing {}", real_path.display()))?; + + println!(" => {}", style(path.to_string_lossy()).dim()); + } + + Ok(()) +} From 57779ed750e69a05186b7765575465d2df46f9c7 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Mon, 10 Jul 2023 14:24:09 +0300 Subject: [PATCH 3/6] mrpack support woo --- DOCS.md | 111 +++++++++++++++++++++++++++++---- README.md | 17 +++-- src/commands/import/mod.rs | 3 + src/downloadable/import_url.rs | 45 ++++++++----- src/util/mod.rs | 1 + src/util/mrpack.rs | 1 + 6 files changed, 146 insertions(+), 32 deletions(-) diff --git a/DOCS.md b/DOCS.md index 93b53f7..a6c94d0 100644 --- a/DOCS.md +++ b/DOCS.md @@ -80,6 +80,25 @@ mcman import url https://modrinth.com/plugin/imageframe mcman import url https://www.spigotmc.org/resources/armorstandeditor-reborn.94503/ ``` +### `mcman import mrpack ` + +Imports a [mrpack](https://docs.modrinth.com/docs/modpacks/format_definition/) file (modrinth modpacks) + +Example usage: + +```sh +# direct link +mcman import mrpack https://cdn.modrinth.com/data/xldzprsQ/versions/xWFqQBjM/Create-Extra-full-1.1.0.mrpack +# only /modpack urls +mcman import mrpack https://modrinth.com/modpack/create-extra +# local file +mcman import mrpack My-Pack.mrpack +``` + +### `mcman import customs` + +Utility tool for re-importing all custom url downloadables in a server. + ## Folder Structure In a normal server environment, everything is in one folder and a big giant mess to navigate. @@ -215,7 +234,7 @@ Default values aren't written back to config - except for `aikars_flags`, `proxy disable = false # false by default # adds your own args -jvm_args = "-Dhello=true" +jvm_args = "-exampleidk" game_args = "--world abc" # use aikar's flags @@ -227,13 +246,20 @@ proxy_flags = false # adds -Dcom.mojang.eula.agree=true # therefore you agree to mojang's eula +# writes eula.txt when on fabric/quilt eula_args = true -# adds --nogui to game args, should set to false on proxies... +# adds --nogui to game args nogui = true # specify -Xmx/-Xms (memory) memory = "2048M" + +# a table of properties +[launcher.properties] +hello="thing" +# ^ same as this: +# jvm_args = "-Dhello=thing" ``` ## Types @@ -244,11 +270,14 @@ Below are some types used in `server.toml` A downloadable is some source of a plugin, mod or a server jar. -Index of types: +Index of sources: - [Vanilla](#vanilla) -- [PaperMC](#papermc) +- [Fabric](#fabric) +- [Quilt](#quilt) +- [PaperMC](#papermc) (Paper, Waterfall and Velocity) - [PurpurMC](#purpurmc) +- [BungeeCord](#bungeecord) - [Modrinth](#modrinth) - [Spigot](#spigot) - [Github Releases](#github-releases) @@ -263,20 +292,53 @@ Used for a vanilla server jar. Has no properties type = "vanilla" ``` +#### Fabric + +The [Fabric](https://fabricmc.net/) mod loader + +**Options:** + +- `type` = `"fabric"` +- `installer`: string | `"latest"` - Installer version to use +- `loader`: string | `"latest"` - Loader version to use + +```toml +type = "fabric" +installer = "latest" +loader = "latest" +``` + +#### Quilt + +The [Quilt](https://quiltmc.org/) project - mod loader compatible with fabric + +Due to some complexities with quilt, `mcman` will need to run `java` to install the quilt server jar - keep this in mind. + +**Options:** + +- `type` = `"quilt"` +- `installer`: string | `"latest"` - Installer version to use +- `loader`: string | `"latest"` - Loader version to use + +```toml +type = "quilt" +installer = "latest" +loader = "latest" +``` + #### PaperMC Allows downloading a [PaperMC](https://papermc.io/) project. **Options:** -- `type` = `papermc` +- `type` = `"papermc"` - `project`: string - The project name - `build`: string | `"latest"` - Optional ```toml # Its recommended to use the shortcuts: type = "paper" -type = "folia" type = "waterfall" type = "velocity" @@ -286,7 +348,7 @@ project = "paper" # Optionally define the build if you dont want to use the latest: type = "papermc" -project = "folia" +project = "waterfall" build = "17" # Note: the shortcuts do not support the 'build' property ``` @@ -297,7 +359,7 @@ Downloads server jar from [PurpurMC](https://purpurmc.org/). **Options:** -- `type` = `purpur` +- `type` = `"purpur"` - `build`: string | `"latest"` - Optional ```toml @@ -308,13 +370,31 @@ build = "10" # if omitted, uses latest ``` +#### BungeeCord + +BungeeCord is just a shortcut to a [jenkins](#jenkins) downloadable: + +```toml +type = "bungeecord" +``` + +If you'd like to get a specific build, use this: + +```toml +type = "jenkins" +url = "https://ci.md-5.net" +job = "BungeeCord" +build = "latest" +artifact = "BungeeCord" +``` + #### Modrinth Downloads from [Modrinth](https://modrinth.com/)'s API **Options:** -- `type` = `modrinth` | `mr` +- `type` = `"modrinth"` | `"mr"` - `id`: string - id of the project or the slug - `version`: string | `"latest"` - Version ID, `"latest"` not recommended @@ -332,7 +412,7 @@ This uses [Spiget](https://spiget.org/)'s API. **Options:** -- `type` = `spigot` +- `type` = `"spigot"` - `id`: string - id of the project You can find the ID of the resource in the URL: @@ -355,7 +435,7 @@ Allows downloading from github releases **Options:** -- `type` = `ghrel` +- `type` = `"ghrel"` - `repo`: string - repository identifier, like `"ParadigmMC/mcman"` - `tag`: string | `"latest"` - The tag of the release - `asset`: string | `"first"` - The name of the asset (checks for inclusion) @@ -376,7 +456,7 @@ Use a jenkins server **Options:** -- `type` = `jenkins` +- `type` = `"jenkins"` - `url`: string - url of the jenkins server - `job`: string - The job - `build`: string | `"latest"` - The build number to use @@ -401,6 +481,13 @@ artifact = "first" Allows you to download from a defined URL. +**Options:** + +- `type` = `"url"` +- `url`: string - URL to the file +- `filename`: string? - Optional filename if you dont like the name from the url +- `desc`: string? - Optional description (shown in markdown tables) + ```toml [[mods]] type = "url" diff --git a/README.md b/README.md index e818987..443991f 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,15 @@ Powerful Minecraft Server Manager CLI. Easily install jars (server, plugins & mo - Always keep up to date with new serverjar builds! - No more manually downloading jars - mcman auto updates them according to your `server.toml` - Supports a variety of [sources](./DOCS.md#downloadable): - - Server jars: + - Servers: - Vanilla - - PaperMC (Paper, Folia, Waterfall and Velocity) + - Fabric + - Quilt + - Paper - PurpurMC + - Velocity + - Waterfall + - BungeeCord - Plugins/Mods: - Modrinth - Spigot @@ -37,6 +42,12 @@ View the [Documentation](./DOCS.md) here. ## Changelog +### `0.3.0` (unreleased) + +- Added **Fabric** support. +- Added **Quilt** support. +- Added `mcman import mrpack` command. + ### `0.2.0` - Wrote more [documentation](./DOCS.md) @@ -49,8 +60,6 @@ View the [Documentation](./DOCS.md) here. - Supports modrinth, modrinth's cdn, github, spigot, jenkins and custom urls. - Also wayy too interactive. For example, it'll ask for which release to use and suggest which asset to use. Similar thing in modrinth importing. - Added **BungeeCord** support. - - - Added **Jenkins** as a source. - Impoved `mcman init` command. It now has a little wizard! - Made mcman build look prettier diff --git a/src/commands/import/mod.rs b/src/commands/import/mod.rs index e9c2c3b..5d811ad 100644 --- a/src/commands/import/mod.rs +++ b/src/commands/import/mod.rs @@ -3,6 +3,7 @@ use clap::{ArgMatches, Command}; mod customs; mod url; +mod mrpack; pub fn cli() -> Command { Command::new("import") @@ -12,11 +13,13 @@ pub fn cli() -> Command { .arg_required_else_help(true) .subcommand(url::cli()) .subcommand(customs::cli()) + .subcommand(mrpack::cli()) } pub async fn run(matches: &ArgMatches) -> Result<()> { match matches.subcommand() { Some(("url", sub_matches)) => url::run(sub_matches).await?, + Some(("mrpack", sub_matches)) => mrpack::run(sub_matches).await?, Some(("customs", _)) => customs::run().await?, _ => unreachable!(), } diff --git a/src/downloadable/import_url.rs b/src/downloadable/import_url.rs index ee22329..00c5bc1 100644 --- a/src/downloadable/import_url.rs +++ b/src/downloadable/import_url.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, bail, Result}; +use console::style; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; use reqwest::Url; @@ -34,6 +35,8 @@ impl Downloadable { Err(invalid_url())?; } + println!(" > {} Modrinth/{id}", style("Imported:").green()); + Ok(Self::Modrinth { id: id.to_owned().to_owned(), version: version.to_owned().to_owned(), @@ -81,7 +84,7 @@ impl Downloadable { } let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which version?") + .with_prompt(" Which version?") .default(0) .items( &versions @@ -100,6 +103,8 @@ impl Downloadable { versions[selection].clone() }; + println!(" > {} Modrinth/{id}", style("Imported:").green()); + Ok(Self::Modrinth { id, version: version.id, @@ -121,6 +126,8 @@ impl Downloadable { .get(1) .ok_or_else(|| anyhow!("Invalid Spigot Resource URL"))?; + println!(" > {} Spigot/{id}", style("Imported:").green()); + Ok(Downloadable::Spigot { id: id.to_owned().to_owned(), }) @@ -154,7 +161,7 @@ impl Downloadable { tag_opt = Some(tag.to_owned()); - println!("> Using release {tag}"); + println!(" > Implied release: {tag}"); } Some("download") => { let invalid_url = || anyhow!("Invalid github release download url"); @@ -163,13 +170,13 @@ impl Downloadable { tag_opt = Some(tag.to_owned()); - println!("> Using release '{tag}'"); + println!(" > Implied release: '{tag}'"); let file = segments.next().ok_or_else(invalid_url)?; file_opt = Some(file); - println!("> Using asset '{tag}'"); + println!(" > Implied asset: '{tag}'"); } Some(p) => bail!("No idea what to do with releases/{p}"), None => {} @@ -188,7 +195,7 @@ impl Downloadable { } let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which release to use?") + .with_prompt(" Which release to use?") .items(&items) .default(0) .interact_opt()? @@ -230,7 +237,7 @@ impl Downloadable { let str_list: Vec = items.iter().map(|t| t.1.clone()).collect(); let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which asset to use?") + .with_prompt(" Which asset to use?") .items(&str_list) .default(idx) .interact_opt()? @@ -241,7 +248,7 @@ impl Downloadable { let inferred = file_opt.unwrap_or(""); let input: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Asset name?") + .with_prompt(" Asset name?") .with_initial_text(inferred) .default(inferred.into()) .interact_text()?; @@ -252,12 +259,15 @@ impl Downloadable { a => a.to_owned(), }; + println!(" > {} Github/{repo}/{tag}/{asset}", style("Imported:").green()); + Ok(Self::GithubRelease { repo, tag, asset }) } Some(_) | None => { let items = vec!["Add as Custom URL", "Add as Jenkins", "Nevermind, cancel"]; let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(" How would you like to import this URL?") .items(&items) .default(0) .interact_opt()?; @@ -273,15 +283,17 @@ impl Downloadable { .unwrap(); let input: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Filename?") + .with_prompt(" Filename?") .with_initial_text(inferred) .default(inferred.into()) .interact_text()?; let desc: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Optional description/comment?") + .with_prompt(" Optional description/comment?") .interact_text()?; + println!(" > {} as URL", style("Imported:").green()); + Ok(Self::Url { url: urlstr.to_owned(), filename: Some(input), @@ -289,16 +301,15 @@ impl Downloadable { }) } Some(1) => { - // TODO: make it better - println!(" >>> https://{}", url.domain().unwrap()); + // TODO: make it better..? let j_url = if Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt("Is this the correct jenkins server url?") + .with_prompt(" Is this the correct jenkins server url?\n > https://".to_owned() + url.domain().unwrap()) .interact()? { "https://".to_owned() + url.domain().unwrap() } else { Input::::with_theme(&ColorfulTheme::default()) - .with_prompt("Jenkins URL:") + .with_prompt(" Jenkins URL:") .with_initial_text(urlstr) .default(urlstr.into()) .interact_text()? @@ -329,23 +340,25 @@ impl Downloadable { }; let job: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Job?") + .with_prompt(" Jenkins Job:") .with_initial_text(&inferred_job) .default(inferred_job) .interact_text()?; let build: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Build:") + .with_prompt(" Jenkins Build:") .with_initial_text("latest") .default("latest".into()) .interact_text()?; let artifact: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Artifact:") + .with_prompt(" Jenkins Artifact:") .with_initial_text("first") .default("first".into()) .interact_text()?; + println!(" > {} Jenkins/{job}", style("Imported:").green()); + Ok(Self::Jenkins { url: j_url, job, diff --git a/src/util/mod.rs b/src/util/mod.rs index d0fceaa..bf19b16 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,5 @@ pub mod md; +pub mod mrpack; use anyhow::{Context, Result}; use futures::StreamExt; diff --git a/src/util/mrpack.rs b/src/util/mrpack.rs index 819919c..5fe0794 100644 --- a/src/util/mrpack.rs +++ b/src/util/mrpack.rs @@ -44,6 +44,7 @@ pub struct Env { } #[derive(Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] pub enum EnvSupport { Required, Optional, From 1d5464f09b393624ec177e363b6939b8fea2ce49 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Mon, 10 Jul 2023 16:09:40 +0300 Subject: [PATCH 4/6] mrpack utils --- DOCS.md | 37 ++++++++++++-- README.md | 1 + res/default_readme | 2 + src/commands/import/mrpack.rs | 65 +++--------------------- src/commands/init.rs | 96 ++++++++++++++++++++++++++++++++++- src/util/md.rs | 44 +++++++++------- src/util/mrpack.rs | 93 +++++++++++++++++++++++++++++++-- 7 files changed, 251 insertions(+), 87 deletions(-) diff --git a/DOCS.md b/DOCS.md index a6c94d0..90c34e4 100644 --- a/DOCS.md +++ b/DOCS.md @@ -13,11 +13,22 @@ Index: Here are a list of commands. You can type `mcman` or `mcman --help` for a basic list of it. -### `mcman init` +### `mcman init [--name ] [--mrpack ]` Initializes a new server in the current directory. -Example: +This command is interactive. Just run `mcman init`! + +The source is the same as one in [`mcman import mrpack`](#mcman-import-mrpack-src) + +Example using [Adrenaserver](https://modrinth.com/modpack/adrenaserver): + +```sh +# these are all identical +mcman init --mrpack mr:adrenaserver +mcman init --mrpack https://modrinth.com/modpack/adrenaserver +mcman init --mrpack https://cdn.modrinth.com/data/H9OFWiay/versions/2WXUgVhc/Adrenaserver-1.4.0%2B1.20.1.quilt.mrpack +``` ### `mcman version` @@ -72,6 +83,13 @@ Example render: Imports a plugin or a mod from a url. +Supports: + +- Modrinth +- Spigot +- Github (releases) +- If not those, will prompt with direct url or jenkins + Example usage: ```sh @@ -80,17 +98,28 @@ mcman import url https://modrinth.com/plugin/imageframe mcman import url https://www.spigotmc.org/resources/armorstandeditor-reborn.94503/ ``` -### `mcman import mrpack ` +### `mcman import mrpack ` Imports a [mrpack](https://docs.modrinth.com/docs/modpacks/format_definition/) file (modrinth modpacks) -Example usage: +**Note:** [`mcman init`](#mcman-init---name-name---mrpack-src) supports mrpacks + +The source can be: + +- A direct URL to a `.mrpack` file +- A local file path +- Modpack URL (`https://modrinth.com/modpack/{id}`) +- Modrinth project id prefixed with `mr:` + +Example usages: ```sh # direct link mcman import mrpack https://cdn.modrinth.com/data/xldzprsQ/versions/xWFqQBjM/Create-Extra-full-1.1.0.mrpack # only /modpack urls mcman import mrpack https://modrinth.com/modpack/create-extra +# prefixed +mcman import mrpack mr:simply-skyblock # local file mcman import mrpack My-Pack.mrpack ``` diff --git a/README.md b/README.md index 443991f..a4f408e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Powerful Minecraft Server Manager CLI. Easily install jars (server, plugins & mo - Modrinth - Spigot - And even **Github Releases**, **Custom URL**s and **Jenkins!** + - Supports importing from [mrpack](./DOCS.md#mcman-import-mrpack-src)s! - Bootstraps your server configuration files - Allows you to use variables inside your config files - Environment variables for secrets diff --git a/res/default_readme b/res/default_readme index 83b087e..f151c33 100644 --- a/res/default_readme +++ b/res/default_readme @@ -2,6 +2,8 @@ [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) + + diff --git a/src/commands/import/mrpack.rs b/src/commands/import/mrpack.rs index 3b97a99..46658ee 100644 --- a/src/commands/import/mrpack.rs +++ b/src/commands/import/mrpack.rs @@ -1,13 +1,11 @@ use std::{path::PathBuf, fs::File}; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result}; use clap::{arg, ArgMatches, Command}; use console::style; -use dialoguer::{Select, theme::ColorfulTheme}; -use reqwest::Url; use tempfile::Builder; -use crate::{commands::version::APP_USER_AGENT, downloadable::{Downloadable, sources::modrinth::{fetch_modrinth_versions, ModrinthVersion}}, model::Server, util::{mrpack::import_from_mrpack, download_with_progress}}; +use crate::{commands::version::APP_USER_AGENT, model::Server, util::{mrpack::{import_from_mrpack, resolve_mrpack_source}, download_with_progress}}; pub fn cli() -> Command { Command::new("mrpack") @@ -27,64 +25,13 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { let tmp_dir = Builder::new().prefix("mcman-mrpack-import").tempdir()?; - let filename = if src.starts_with("http") { - println!(" > {}", style("Downloading mrpack...").green()); - + let filename = if src.starts_with("http") || src.starts_with("mr:") { let fname = tmp_dir.path().join("pack.mrpack"); let file = tokio::fs::File::create(&fname).await?; + + let downloadable = resolve_mrpack_source(src, &http_client).await?; - let url = Url::parse(src)?; - - let modpack_id = { - if url.domain() == Some("modrinth.com") - && url.path().starts_with("/modpack") { - url.path_segments() - .ok_or(anyhow!("Invalid modrinth /modpack URL"))? - .nth(1) - } else { - None - } - }; - - let downloadable = if let Some(id) = modpack_id { - let versions: Vec = fetch_modrinth_versions(&http_client, &id, None) - .await? - .into_iter() - .filter(|v| v.game_versions.contains(&server.mc_version)) - .collect(); - - let version = { - if versions.is_empty() { - bail!("No compatible versions in modrinth project"); - } - - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt(" Which version?") - .default(0) - .items( - &versions - .iter() - .map(|v| { - let num = &v.version_number; - let name = &v.name; - let compat = v.loaders.join(","); - format!("[{num}] {name} / {compat}") - }) - .collect::>(), - ) - .interact() - .unwrap(); - - versions[selection].clone() - }; - - Downloadable::Modrinth { - id: id.to_owned(), - version: version.id, - } - } else { - Downloadable::Url { url: src.clone(), filename: None, desc: None } - }; + println!(" > {}", style("Downloading mrpack...").green()); download_with_progress( file, diff --git a/src/commands/init.rs b/src/commands/init.rs index 4b92e83..167bc91 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,12 +1,15 @@ use console::style; use dialoguer::Confirm; use dialoguer::{theme::ColorfulTheme, Input, Select}; +use tempfile::Builder; use std::fs::File; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::ffi::OsStr; use crate::commands::markdown; +use crate::util::download_with_progress; +use crate::util::mrpack::{import_from_mrpack, resolve_mrpack_source}; use crate::{ commands::version::APP_USER_AGENT, downloadable::{sources::vanilla::fetch_latest_mcver, Downloadable}, @@ -19,6 +22,7 @@ pub fn cli() -> Command { Command::new("init") .about("Initializes a new MCMan-powered Minecraft server") .arg(arg!(--name "The name of the server").required(false)) + .arg(arg!(--mrpack "Import from a modrinth modpack").required(false)) } pub async fn run(matches: &ArgMatches) -> Result<()> { @@ -58,6 +62,96 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { .with_initial_text(&name) .interact_text()?; + if let Some(src) = matches.get_one::("mrpack") { + println!(" > {}", style("Importing from mrpack...").cyan()); + + let tmp_dir = Builder::new().prefix("mcman-mrpack-import").tempdir()?; + + let mut server = Server { + name, + ..Default::default() + }; + + let filename = if src.starts_with("http") || src.starts_with("mr:") { + let fname = tmp_dir.path().join("pack.mrpack"); + let file = tokio::fs::File::create(&fname).await?; + + let downloadable = resolve_mrpack_source(src, &http_client).await?; + + println!(" > {}", style("Downloading mrpack...").green()); + + download_with_progress( + file, + &format!("Downloading {src}..."), + &downloadable, + &server, + &http_client, + ).await?; + + fname + } else { + PathBuf::from(src) + }; + + let f = File::open(filename).context("opening file")?; + + let pack = import_from_mrpack(&mut server, &http_client, f).await?; + + server.mc_version = if let Some(v) = pack.dependencies.get("minecraft") { + v.clone() + } else { + let latest_ver = fetch_latest_mcver(&http_client) + .await + .context("Fetching latest version")?; + + Input::with_theme(&theme) + .with_prompt("Server version?") + .default(latest_ver) + .interact_text()? + }; + + server.jar = { + if let Some(ver) = pack.dependencies.get("fabric-loader") { + println!(" > {} {}", style("Using fabric loader").cyan(), style(ver).bold()); + Downloadable::Fabric { loader: ver.clone(), installer: "latest".to_owned() } + } else { + if let Some(ver) = pack.dependencies.get("quilt-loader") { + println!(" > {} {}", style("Using quilt loader").cyan(), style(ver).bold()); + Downloadable::Quilt { loader: ver.clone(), installer: "latest".to_owned() } + } else { + Downloadable::select_modded_jar_interactive()? + } + } + }; + + println!(" > {}", style("Imported .mrpack!").green()); + + initialize_environment(false).context("Initializing environment")?; + server.save()?; + + let write_readme = if Path::new("./README.md").exists() { + Confirm::with_theme(&theme) + .default(true) + .with_prompt("Overwrite README.md?") + .interact()? + } else { + true + }; + + if write_readme { + markdown::initialize_readme(&server).context("Initializing readme")?; + } + + println!(" > {}", style("Server has been initialized!").cyan()); + println!( + " > {} {}", + style("Build using").cyan(), + style("mcman build").bold() + ); + + return Ok(()); + } + let serv_type = Select::with_theme(&theme) .with_prompt("Type of server?") .default(0) diff --git a/src/util/md.rs b/src/util/md.rs index d264ea0..5e218ce 100644 --- a/src/util/md.rs +++ b/src/util/md.rs @@ -92,7 +92,7 @@ impl MarkdownTable { lines.join("\n") } - pub fn render_ascii(&self) -> String { + pub fn render_ascii_lines(&self, headers: bool) -> Vec { let mut col_lengths = vec![]; for idx in 0..self.headers.len() { @@ -109,23 +109,25 @@ impl MarkdownTable { let mut lines = vec![]; - lines.push({ - let mut cols = vec![]; - for (idx, width) in col_lengths.iter().enumerate() { - cols.push(format!("{:width$}", self.headers[idx])); - } - - cols.join(" ") - }); - - lines.push({ - let mut cols = vec![]; - for length in &col_lengths { - cols.push(format!("{:-^width$}", "", width = length)); - } - - cols.join(" ") - }); + if headers { + lines.push({ + let mut cols = vec![]; + for (idx, width) in col_lengths.iter().enumerate() { + cols.push(format!("{:width$}", self.headers[idx])); + } + + cols.join(" ") + }); + + lines.push({ + let mut cols = vec![]; + for length in &col_lengths { + cols.push(format!("{:-^width$}", "", width = length)); + } + + cols.join(" ") + }); + } for row in &self.rows { lines.push({ @@ -138,6 +140,10 @@ impl MarkdownTable { }); } - lines.join("\n") + lines + } + + pub fn render_ascii(&self) -> String { + self.render_ascii_lines(true).join("\n") } } diff --git a/src/util/mrpack.rs b/src/util/mrpack.rs index 5fe0794..4caeb47 100644 --- a/src/util/mrpack.rs +++ b/src/util/mrpack.rs @@ -1,15 +1,25 @@ use anyhow::{Context, Result}; use console::style; +use dialoguer::{Select, theme::ColorfulTheme}; +use indexmap::IndexMap; +use reqwest::Url; use serde::{Deserialize, Serialize}; -use std::{io::{Read, Seek}, path::PathBuf, fs}; +use std::{io::{Read, Seek}, path::PathBuf, fs, collections::HashMap}; use zip::ZipArchive; -use crate::{model::Server, downloadable::Downloadable}; +use crate::{model::Server, downloadable::{Downloadable, sources::modrinth::{ModrinthVersion, fetch_modrinth_versions}}}; + +use super::md::MarkdownTable; #[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct MRPackIndex { + pub game: String, pub name: String, + pub version_id: String, + pub summary: Option, pub files: Vec, + pub dependencies: HashMap, } impl MRPackIndex { @@ -55,7 +65,7 @@ pub async fn import_from_mrpack( server: &mut Server, http_client: &reqwest::Client, reader: R, -) -> Result<()> { +) -> Result { println!( " > {}", style("Importing mrpack...").cyan(), @@ -125,5 +135,80 @@ pub async fn import_from_mrpack( println!(" => {}", style(path.to_string_lossy()).dim()); } - Ok(()) + Ok(pack) +} + +pub fn select_modrinth_version( + list: Vec, + server: Option, +) -> ModrinthVersion { + let mut table = MarkdownTable::new(); + + let list = list + .iter() + .filter(|v| { + if let Some(serv) = &server { + if !v.game_versions.contains(&serv.mc_version) { + return false; + } + } + true + }).collect::>(); + + for v in &list { + let mut map = IndexMap::new(); + + map.insert("num".to_owned(), v.version_number.clone()); + map.insert("name".to_owned(), v.name.clone()); + map.insert("compat".to_owned(), v.loaders.join(",")); + + table.add_from_map(&map); + } + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(" Which version?") + .default(0) + .items( + &table.render_ascii_lines(false) + ) + .interact() + .unwrap(); + + list[selection].clone() +} + +pub async fn resolve_mrpack_source( + src: &str, + http_client: &reqwest::Client, +) -> Result { + println!(" > {}", style("Resolving mrpack...").green()); + + let modpack_id = if src.starts_with("mr:") { + Some(src.strip_prefix("mr:").unwrap().to_owned()) + } else { + let url = Url::parse(src)?; + + if url.domain() == Some("modrinth.com") + && url.path().starts_with("/modpack") { + url.path().strip_prefix("/modpack/").map(|i| i.to_owned()) + } else { + None + } + }; + + let downloadable = if let Some(id) = modpack_id { + let versions: Vec = fetch_modrinth_versions(&http_client, &id, None) + .await?; + + let version = select_modrinth_version(versions, None); + + Downloadable::Modrinth { + id: id.to_owned(), + version: version.id, + } + } else { + Downloadable::Url { url: src.to_owned(), filename: None, desc: None } + }; + + Ok(downloadable) } From 6b0b552de261c6fb5af3a8bff016f64d62b02593 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Mon, 10 Jul 2023 16:18:14 +0300 Subject: [PATCH 5/6] cargo clippy && fmt --- src/bootstrapper/mod.rs | 12 +++- src/commands/build.rs | 62 +++++++++--------- src/commands/import/mod.rs | 2 +- src/commands/import/mrpack.rs | 16 +++-- src/commands/init.rs | 50 +++++++++----- src/downloadable/import_url.rs | 11 +++- src/downloadable/markdown.rs | 7 +- src/downloadable/mod.rs | 4 +- src/downloadable/sources/github.rs | 57 +++++++++------- src/downloadable/sources/quilt.rs | 23 +++---- src/util/md.rs | 6 +- src/util/mrpack.rs | 101 +++++++++++++++++------------ 12 files changed, 207 insertions(+), 144 deletions(-) diff --git a/src/bootstrapper/mod.rs b/src/bootstrapper/mod.rs index 3eec95b..fd65fb9 100644 --- a/src/bootstrapper/mod.rs +++ b/src/bootstrapper/mod.rs @@ -53,7 +53,10 @@ where continue; } - bootstrap_entry(ctx, &entry).context(format!("Bootstrapping [{}]", entry.path().to_string_lossy()))?; + bootstrap_entry(ctx, &entry).context(format!( + "Bootstrapping [{}]", + entry.path().to_string_lossy() + ))?; } Ok(()) @@ -64,8 +67,11 @@ fn bootstrap_entry(ctx: &BootstrapContext, entry: &DirEntry) -> Result<()> { let output_path = ctx.get_output_path(path); if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent) - .context(format!("Creating parent directory of [{}], dir = [{}]", output_path.to_string_lossy(), parent.to_string_lossy()))?; + std::fs::create_dir_all(parent).context(format!( + "Creating parent directory of [{}], dir = [{}]", + output_path.to_string_lossy(), + parent.to_string_lossy() + ))?; } // bootstrap contents of some types if let Some(ext) = path.extension() { diff --git a/src/commands/build.rs b/src/commands/build.rs index 7b82358..f87f135 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -2,12 +2,13 @@ use std::{ collections::HashMap, env, fs::{self, OpenOptions}, - io::{Write, BufReader, BufRead}, + io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, - time::{Instant, Duration}, process::Stdio + process::Stdio, + time::{Duration, Instant}, }; -use anyhow::{Context, Result, bail}; +use anyhow::{bail, Context, Result}; use clap::{arg, value_parser, ArgMatches, Command}; use console::{style, Style}; use indicatif::{ProgressBar, ProgressStyle}; @@ -32,7 +33,7 @@ pub fn cli() -> Command { .arg( arg!(--skip [stages] "Skip some stages") .value_delimiter(',') - .default_value("") + .default_value(""), ) } @@ -64,7 +65,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { println!(" stage {stage_index}: {}", title.apply_to(stage_name)); stage_index += 1; }; - + let mark_stage_skipped = |id| { println!(" {}{id}", style("-> Skipping stage ").yellow().bold()); }; @@ -84,7 +85,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { .await .context("Failed to download plugins")?; } - + // stage 3: mods if !server.mods.is_empty() { mark_stage("Mods"); @@ -123,11 +124,12 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { if server.launcher.eula_args { match server.jar { - Downloadable::Quilt { .. } - | Downloadable::Fabric { .. } => { - println!(" {}", style("=> eula.txt [eula_args unsupported]").dim()); - std::fs::File::create(output_dir.join("eula.txt"))? - .write_all(b"eula=true")?; + Downloadable::Quilt { .. } | Downloadable::Fabric { .. } => { + println!( + " {}", + style("=> eula.txt [eula_args unsupported]").dim() + ); + std::fs::File::create(output_dir.join("eula.txt"))?.write_all(b"eula=true")?; } _ => (), } @@ -157,6 +159,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { Ok(()) } +#[allow(clippy::too_many_lines)] async fn download_server_jar( server: &Server, http_client: &reqwest::Client, @@ -189,7 +192,8 @@ async fn download_server_jar( .await?; } - let serverjar_name = format!("quilt-server-launch-{}-{}.jar", server.mc_version, loader); + let serverjar_name = + format!("quilt-server-launch-{}-{}.jar", server.mc_version, loader); if output_dir.join(serverjar_name.clone()).exists() { println!( @@ -211,7 +215,7 @@ async fn download_server_jar( ]; if loader != "latest" { - args.push(&loader); + args.push(loader); } args.push("--install-dir=."); @@ -224,13 +228,12 @@ async fn download_server_jar( .spawn() .context("Running quilt-server-installer")?; - let spinner = ProgressBar::new_spinner() - .with_style( - ProgressStyle::with_template(" {spinner:.dim.bold} {msg}")? - ); + let spinner = ProgressBar::new_spinner().with_style(ProgressStyle::with_template( + " {spinner:.dim.bold} {msg}", + )?); spinner.enable_steady_tick(Duration::from_millis(200)); - + let prefix = style("[qsi]").bold(); for line in BufReader::new(child.stdout.take().unwrap()).lines() { @@ -240,7 +243,7 @@ async fn download_server_jar( spinner.set_message(format!("{prefix} {stripped_line}")); } } - + if !child.wait()?.success() { bail!("Quilt server installer exited with non-zero code"); } @@ -255,11 +258,12 @@ async fn download_server_jar( fs::rename( output_dir.join("quilt-server-launch.jar"), output_dir.join(&serverjar_name), - ).context("Renaming quilt-server-launch.jar")?; + ) + .context("Renaming quilt-server-launch.jar")?; } serverjar_name - }, + } dl => { let serverjar_name = dl.get_filename(server, http_client).await?; if output_dir.join(serverjar_name.clone()).exists() { @@ -272,24 +276,24 @@ async fn download_server_jar( " Downloading server jar ({})", style(serverjar_name.clone()).dim() ); - + let filename = &dl.get_filename(server, http_client).await?; util::download_with_progress( File::create(&output_dir.join(filename)) .await .context(format!("Failed to create output file for {filename}"))?, filename, - &dl, + dl, server, http_client, ) .await?; } - + serverjar_name } }; - + Ok(serverjar_name) } @@ -356,11 +360,7 @@ async fn download_addons( Ok(()) } -fn create_scripts( - server: &Server, - serverjar_name: &str, - output_dir: &Path, -) -> Result<()> { +fn create_scripts(server: &Server, serverjar_name: &str, output_dir: &Path) -> Result<()> { fs::write( output_dir.join("start.bat"), server @@ -399,4 +399,4 @@ fn create_scripts( ); Ok(()) -} \ No newline at end of file +} diff --git a/src/commands/import/mod.rs b/src/commands/import/mod.rs index 5d811ad..3c43043 100644 --- a/src/commands/import/mod.rs +++ b/src/commands/import/mod.rs @@ -2,8 +2,8 @@ use anyhow::Result; use clap::{ArgMatches, Command}; mod customs; -mod url; mod mrpack; +mod url; pub fn cli() -> Command { Command::new("import") diff --git a/src/commands/import/mrpack.rs b/src/commands/import/mrpack.rs index 46658ee..b9cd6bf 100644 --- a/src/commands/import/mrpack.rs +++ b/src/commands/import/mrpack.rs @@ -1,11 +1,18 @@ -use std::{path::PathBuf, fs::File}; +use std::{fs::File, path::PathBuf}; use anyhow::{Context, Result}; use clap::{arg, ArgMatches, Command}; use console::style; use tempfile::Builder; -use crate::{commands::version::APP_USER_AGENT, model::Server, util::{mrpack::{import_from_mrpack, resolve_mrpack_source}, download_with_progress}}; +use crate::{ + commands::version::APP_USER_AGENT, + model::Server, + util::{ + download_with_progress, + mrpack::{import_from_mrpack, resolve_mrpack_source}, + }, +}; pub fn cli() -> Command { Command::new("mrpack") @@ -28,7 +35,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { let filename = if src.starts_with("http") || src.starts_with("mr:") { let fname = tmp_dir.path().join("pack.mrpack"); let file = tokio::fs::File::create(&fname).await?; - + let downloadable = resolve_mrpack_source(src, &http_client).await?; println!(" > {}", style("Downloading mrpack...").green()); @@ -39,7 +46,8 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { &downloadable, &server, &http_client, - ).await?; + ) + .await?; fname } else { diff --git a/src/commands/init.rs b/src/commands/init.rs index 167bc91..618569e 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,11 +1,11 @@ use console::style; use dialoguer::Confirm; use dialoguer::{theme::ColorfulTheme, Input, Select}; -use tempfile::Builder; +use std::ffi::OsStr; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; -use std::ffi::OsStr; +use tempfile::Builder; use crate::commands::markdown; use crate::util::download_with_progress; @@ -25,6 +25,7 @@ pub fn cli() -> Command { .arg(arg!(--mrpack "Import from a modrinth modpack").required(false)) } +#[allow(clippy::too_many_lines)] pub async fn run(matches: &ArgMatches) -> Result<()> { let http_client = reqwest::Client::builder() .user_agent(APP_USER_AGENT) @@ -73,9 +74,9 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { }; let filename = if src.starts_with("http") || src.starts_with("mr:") { - let fname = tmp_dir.path().join("pack.mrpack"); - let file = tokio::fs::File::create(&fname).await?; - + let filename = tmp_dir.path().join("pack.mrpack"); + let file = tokio::fs::File::create(&filename).await?; + let downloadable = resolve_mrpack_source(src, &http_client).await?; println!(" > {}", style("Downloading mrpack...").green()); @@ -86,9 +87,10 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { &downloadable, &server, &http_client, - ).await?; + ) + .await?; - fname + filename } else { PathBuf::from(src) }; @@ -101,8 +103,8 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { v.clone() } else { let latest_ver = fetch_latest_mcver(&http_client) - .await - .context("Fetching latest version")?; + .await + .context("Fetching latest version")?; Input::with_theme(&theme) .with_prompt("Server version?") @@ -111,16 +113,28 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { }; server.jar = { - if let Some(ver) = pack.dependencies.get("fabric-loader") { - println!(" > {} {}", style("Using fabric loader").cyan(), style(ver).bold()); - Downloadable::Fabric { loader: ver.clone(), installer: "latest".to_owned() } - } else { - if let Some(ver) = pack.dependencies.get("quilt-loader") { - println!(" > {} {}", style("Using quilt loader").cyan(), style(ver).bold()); - Downloadable::Quilt { loader: ver.clone(), installer: "latest".to_owned() } - } else { - Downloadable::select_modded_jar_interactive()? + if let Some(ver) = pack.dependencies.get("quilt-loader") { + println!( + " > {} {}", + style("Using quilt loader").cyan(), + style(ver).bold() + ); + Downloadable::Quilt { + loader: ver.clone(), + installer: "latest".to_owned(), } + } else if let Some(ver) = pack.dependencies.get("fabric-loader") { + println!( + " > {} {}", + style("Using fabric loader").cyan(), + style(ver).bold() + ); + Downloadable::Fabric { + loader: ver.clone(), + installer: "latest".to_owned(), + } + } else { + Downloadable::select_modded_jar_interactive()? } }; diff --git a/src/downloadable/import_url.rs b/src/downloadable/import_url.rs index 00c5bc1..3180561 100644 --- a/src/downloadable/import_url.rs +++ b/src/downloadable/import_url.rs @@ -259,7 +259,10 @@ impl Downloadable { a => a.to_owned(), }; - println!(" > {} Github/{repo}/{tag}/{asset}", style("Imported:").green()); + println!( + " > {} Github/{repo}/{tag}/{asset}", + style("Imported:").green() + ); Ok(Self::GithubRelease { repo, tag, asset }) } @@ -303,7 +306,11 @@ impl Downloadable { Some(1) => { // TODO: make it better..? let j_url = if Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt(" Is this the correct jenkins server url?\n > https://".to_owned() + url.domain().unwrap()) + .with_prompt( + " Is this the correct jenkins server url?\n > https://" + .to_owned() + + url.domain().unwrap(), + ) .interact()? { "https://".to_owned() + url.domain().unwrap() diff --git a/src/downloadable/markdown.rs b/src/downloadable/markdown.rs index 0354c3a..c249ab9 100644 --- a/src/downloadable/markdown.rs +++ b/src/downloadable/markdown.rs @@ -293,5 +293,10 @@ static SANITIZE_R1: &str = "<(?:\"[^\"]*\"['\"]*|'[^']*'['\"]*|[^'\">])+>"; fn sanitize(s: &str) -> Result { let re = Regex::new(SANITIZE_R1)?; - Ok(re.replace_all(&s.replace('\n', " ").replace('\r', "").replace("
", " "), "").to_string()) + Ok(re + .replace_all( + &s.replace('\n', " ").replace('\r', "").replace("
", " "), + "", + ) + .to_string()) } diff --git a/src/downloadable/mod.rs b/src/downloadable/mod.rs index 4e8aa56..a31c478 100644 --- a/src/downloadable/mod.rs +++ b/src/downloadable/mod.rs @@ -169,9 +169,7 @@ impl Downloadable { Ok(download_fabric(client, &mcver, loader, installer).await?) } - Self::Quilt { installer, .. } => { - Ok(download_quilt_installer(client, installer).await?) - } + Self::Quilt { installer, .. } => Ok(download_quilt_installer(client, installer).await?), } } diff --git a/src/downloadable/sources/github.rs b/src/downloadable/sources/github.rs index a659562..1a3faff 100644 --- a/src/downloadable/sources/github.rs +++ b/src/downloadable/sources/github.rs @@ -10,7 +10,9 @@ async fn wait_ratelimit(res: reqwest::Response) -> Result { if let Some(h) = res.headers().get("x-ratelimit-remaining") { if String::from_utf8_lossy(h.as_bytes()) == "1" { let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - let ratelimit_reset = String::from_utf8_lossy(res.headers()["x-ratelimit-reset"].as_bytes()).parse::()?; + let ratelimit_reset = + String::from_utf8_lossy(res.headers()["x-ratelimit-reset"].as_bytes()) + .parse::()?; let amount = ratelimit_reset - now; println!(" (!) Github ratelimit exceeded. sleeping for {amount} seconds..."); sleep(Duration::from_secs(amount)).await; @@ -37,13 +39,16 @@ pub async fn fetch_github_releases( repo: &str, client: &reqwest::Client, ) -> Result> { - let releases: Vec = wait_ratelimit(client - .get("https://api.github.com/repos/".to_owned() + repo + "/releases") - .send() - .await? - .error_for_status()?).await? - .json() - .await?; + let releases: Vec = wait_ratelimit( + client + .get("https://api.github.com/repos/".to_owned() + repo + "/releases") + .send() + .await? + .error_for_status()?, + ) + .await? + .json() + .await?; Ok(releases) } @@ -80,7 +85,9 @@ pub async fn fetch_github_release_filename( asset: &str, client: &reqwest::Client, ) -> Result { - Ok(fetch_github_release_asset(repo, tag, asset, client).await?.name) + Ok(fetch_github_release_asset(repo, tag, asset, client) + .await? + .name) } // youre delusional, this doesnt exist @@ -109,22 +116,28 @@ pub async fn download_github_release( ) -> Result { let fetched_asset = fetch_github_release_asset(repo, tag, asset, client).await?; - Ok(wait_ratelimit(client - .get(fetched_asset.url) - .header("Accept", "application/octet-stream") - .send() - .await?).await? - .error_for_status()?) + Ok(wait_ratelimit( + client + .get(fetched_asset.url) + .header("Accept", "application/octet-stream") + .send() + .await?, + ) + .await? + .error_for_status()?) } pub async fn fetch_repo_description(client: &reqwest::Client, repo: &str) -> Result { - let desc = wait_ratelimit(client - .get("https://api.github.com/repos/".to_owned() + repo) - .send() - .await?).await? - .error_for_status()? - .json::() - .await?["description"] + let desc = wait_ratelimit( + client + .get("https://api.github.com/repos/".to_owned() + repo) + .send() + .await?, + ) + .await? + .error_for_status()? + .json::() + .await?["description"] .as_str() .unwrap_or_default() .to_owned(); diff --git a/src/downloadable/sources/quilt.rs b/src/downloadable/sources/quilt.rs index 056a320..6af453a 100644 --- a/src/downloadable/sources/quilt.rs +++ b/src/downloadable/sources/quilt.rs @@ -2,7 +2,7 @@ #![allow(unused)] use anyhow::{anyhow, Result}; -use mcapi::quilt::{InstallerVariant, self}; +use mcapi::quilt::{self, InstallerVariant}; pub async fn download_quilt_installer( client: &reqwest::Client, @@ -16,19 +16,17 @@ pub async fn download_quilt_installer( Ok(quilt::download_installer(client, &InstallerVariant::Universal, &v).await?) } -pub async fn fetch_latest_quilt_installer( - client: &reqwest::Client -) -> Result { +pub async fn fetch_latest_quilt_installer(client: &reqwest::Client) -> Result { Ok( - mcapi::quilt::fetch_installer_versions(client, &InstallerVariant::Universal).await? - .last().expect("latest quilt installer version to be present").clone() + mcapi::quilt::fetch_installer_versions(client, &InstallerVariant::Universal) + .await? + .last() + .expect("latest quilt installer version to be present") + .clone(), ) } -pub async fn get_installer_filename( - client: &reqwest::Client, - installer: &str, -) -> Result { +pub async fn get_installer_filename(client: &reqwest::Client, installer: &str) -> Result { let v = match installer { "latest" => fetch_latest_quilt_installer(client).await?, id => id.to_owned(), @@ -37,8 +35,7 @@ pub async fn get_installer_filename( Ok(format!("quilt-installer-{v}.jar")) } - -/* +/* pub async fn fetch_quilt_latest_loader(client: &reqwest::Client) -> Result { let loaders = mcapi::quilt::fetch_loaders(client).await?; @@ -90,4 +87,4 @@ pub async fn get_quilt_filename( Ok(format!("quilt-server-{mcver}-{l}-launch.jar")) } - */ \ No newline at end of file + */ diff --git a/src/util/md.rs b/src/util/md.rs index 5e218ce..3590a09 100644 --- a/src/util/md.rs +++ b/src/util/md.rs @@ -115,16 +115,16 @@ impl MarkdownTable { for (idx, width) in col_lengths.iter().enumerate() { cols.push(format!("{:width$}", self.headers[idx])); } - + cols.join(" ") }); - + lines.push({ let mut cols = vec![]; for length in &col_lengths { cols.push(format!("{:-^width$}", "", width = length)); } - + cols.join(" ") }); } diff --git a/src/util/mrpack.rs b/src/util/mrpack.rs index 4caeb47..9c33c53 100644 --- a/src/util/mrpack.rs +++ b/src/util/mrpack.rs @@ -1,13 +1,24 @@ use anyhow::{Context, Result}; use console::style; -use dialoguer::{Select, theme::ColorfulTheme}; +use dialoguer::{theme::ColorfulTheme, Select}; use indexmap::IndexMap; use reqwest::Url; use serde::{Deserialize, Serialize}; -use std::{io::{Read, Seek}, path::PathBuf, fs, collections::HashMap}; +use std::{ + collections::HashMap, + fs, + io::{Read, Seek}, + path::PathBuf, +}; use zip::ZipArchive; -use crate::{model::Server, downloadable::{Downloadable, sources::modrinth::{ModrinthVersion, fetch_modrinth_versions}}}; +use crate::{ + downloadable::{ + sources::modrinth::{fetch_modrinth_versions, ModrinthVersion}, + Downloadable, + }, + model::Server, +}; use super::md::MarkdownTable; @@ -28,12 +39,13 @@ impl MRPackIndex { server: &mut Server, http_client: &reqwest::Client, ) -> Result<()> { - for f in self.files.iter().filter(|f| f.env.is_none() || f.env.as_ref().unwrap().server != EnvSupport::Unsupported) { + for f in self.files.iter().filter(|f| { + f.env.is_none() || f.env.as_ref().unwrap().server != EnvSupport::Unsupported + }) { let url = f.downloads.first().context("unwrap url from downloads")?; - - let dl = Downloadable::from_url_interactive(http_client, server, url) - .await?; - + + let dl = Downloadable::from_url_interactive(http_client, server, url).await?; + server.mods.push(dl); } Ok(()) @@ -66,10 +78,7 @@ pub async fn import_from_mrpack( http_client: &reqwest::Client, reader: R, ) -> Result { - println!( - " > {}", - style("Importing mrpack...").cyan(), - ); + println!(" > {}", style("Importing mrpack...").cyan(),); let mut archive = ZipArchive::new(reader).context("reading mrpack zip archive")?; @@ -81,7 +90,6 @@ pub async fn import_from_mrpack( .read_to_end(&mut zip_content) .context("reading modrinth.index.json from zip file")?; - let pack: MRPackIndex = serde_json::from_slice(&zip_content)?; pack.import_all(server, http_client) .await @@ -106,14 +114,18 @@ pub async fn import_from_mrpack( continue; } - server.path.join("config").join(path.strip_prefix("overrides")?) + server + .path + .join("config") + .join(path.strip_prefix("overrides")?) + } else if path.starts_with("server-overrides") { + server_overrided.push(path.clone()); + server + .path + .join("config") + .join(path.strip_prefix("server-overrides")?) } else { - if path.starts_with("server-overrides") { - server_overrided.push(path.clone()); - server.path.join("config").join(path.strip_prefix("server-overrides")?) - } else { - continue; - } + continue; }; queue.push((path, real_path)); @@ -126,11 +138,12 @@ pub async fn import_from_mrpack( .read_to_end(&mut zip_content) .context(format!("reading {} from zip file", path.display()))?; - fs::create_dir_all(real_path.parent().unwrap()) - .context(format!("Creating parent folder for {}", real_path.display()))?; + fs::create_dir_all(real_path.parent().unwrap()).context(format!( + "Creating parent folder for {}", + real_path.display() + ))?; - fs::write(&real_path, zip_content) - .context(format!("Writing {}", real_path.display()))?; + fs::write(&real_path, zip_content).context(format!("Writing {}", real_path.display()))?; println!(" => {}", style(path.to_string_lossy()).dim()); } @@ -139,21 +152,22 @@ pub async fn import_from_mrpack( } pub fn select_modrinth_version( - list: Vec, - server: Option, + list: &[ModrinthVersion], + server: &Option, ) -> ModrinthVersion { let mut table = MarkdownTable::new(); let list = list .iter() .filter(|v| { - if let Some(serv) = &server { - if !v.game_versions.contains(&serv.mc_version) { - return false; + if let Some(serv) = &server { + if !v.game_versions.contains(&serv.mc_version) { + return false; + } } - } - true - }).collect::>(); + true + }) + .collect::>(); for v in &list { let mut map = IndexMap::new(); @@ -168,9 +182,7 @@ pub fn select_modrinth_version( let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt(" Which version?") .default(0) - .items( - &table.render_ascii_lines(false) - ) + .items(&table.render_ascii_lines(false)) .interact() .unwrap(); @@ -188,26 +200,29 @@ pub async fn resolve_mrpack_source( } else { let url = Url::parse(src)?; - if url.domain() == Some("modrinth.com") - && url.path().starts_with("/modpack") { - url.path().strip_prefix("/modpack/").map(|i| i.to_owned()) + if url.domain() == Some("modrinth.com") && url.path().starts_with("/modpack") { + url.path().strip_prefix("/modpack/").map(str::to_owned) } else { None } }; let downloadable = if let Some(id) = modpack_id { - let versions: Vec = fetch_modrinth_versions(&http_client, &id, None) - .await?; + let versions: Vec = + fetch_modrinth_versions(http_client, &id, None).await?; - let version = select_modrinth_version(versions, None); + let version = select_modrinth_version(&versions, &None); Downloadable::Modrinth { - id: id.to_owned(), + id: id.clone(), version: version.id, } } else { - Downloadable::Url { url: src.to_owned(), filename: None, desc: None } + Downloadable::Url { + url: src.to_owned(), + filename: None, + desc: None, + } }; Ok(downloadable) From 4d247e733b33155af19d301bca7fa53b9e38f5b9 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Mon, 10 Jul 2023 16:25:47 +0300 Subject: [PATCH 6/6] oooh my bad --- Cargo.lock | 3 ++- Cargo.toml | 6 +++--- README.md | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a5ba1f..4ae9f5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,6 +754,7 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "mcapi" version = "0.2.0" +source = "git+https://github.com/ParadigmMC/mcapi.git#f33186b6b008769538d019cb1e0cacd0389f1e1f" dependencies = [ "os-version", "regex", @@ -766,7 +767,7 @@ dependencies = [ [[package]] name = "mcman" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 95198e0..0d8dfd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mcman" -version = "0.2.0" +version = "0.2.1" edition = "2021" authors = ["ParadigmMC"] repository = "https://github.com/ParadigmMC/mcman" @@ -19,8 +19,8 @@ futures = "0.3" indexmap = "2.0.0" indicatif = "0.17" java-properties = { git = "https://github.com/ParadigmMC/java-properties.git" } -#mcapi = { git = "https://github.com/ParadigmMC/mcapi.git" } -mcapi = { path = "../mcapi" } +mcapi = { git = "https://github.com/ParadigmMC/mcapi.git" } +#mcapi = { path = "../mcapi" } pathdiff = { git = "https://github.com/Manishearth/pathdiff.git" } regex = "1.8" reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"], default-features = false } diff --git a/README.md b/README.md index a4f408e..4ee7dd8 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,12 @@ View the [Documentation](./DOCS.md) here. ## Changelog -### `0.3.0` (unreleased) +### `0.2.1` - Added **Fabric** support. - Added **Quilt** support. - Added `mcman import mrpack` command. +- `mcman init` now supports mrpacks ### `0.2.0`