From 63e1d170bc2dc7da864c8b3847b45e2673a0108c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 13 Apr 2026 22:44:17 +0200 Subject: [PATCH] wip papermc cli stuff --- Cargo.lock | 2 + alex/Cargo.toml | 2 + alex/src/cli/mod.rs | 5 + alex/src/cli/papermc.rs | 210 ++++++++++++++++++++++++++++++++++++++++ alex/src/error.rs | 2 + 5 files changed, 221 insertions(+) create mode 100644 alex/src/cli/papermc.rs diff --git a/Cargo.lock b/Cargo.lock index 6e569bb..c1afa68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,8 +16,10 @@ dependencies = [ "chrono", "clap", "figment", + "papermc-api", "serde", "signal-hook", + "ureq", ] [[package]] diff --git a/alex/Cargo.toml b/alex/Cargo.toml index 7f7cac8..71c1c93 100644 --- a/alex/Cargo.toml +++ b/alex/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] backup = { path = "../backup" } +papermc-api = { path = "../papermc-api" } chrono.workspace = true serde.workspace = true @@ -13,3 +14,4 @@ serde.workspace = true clap = { version = "4.5.37", features = ["derive", "env"] } signal-hook = "0.3.15" figment = { version = "0.10.10", features = ["env", "toml"] } +ureq = "3.3.0" diff --git a/alex/src/cli/mod.rs b/alex/src/cli/mod.rs index 47d12f3..92724ea 100644 --- a/alex/src/cli/mod.rs +++ b/alex/src/cli/mod.rs @@ -1,5 +1,6 @@ mod backup; mod config; +mod papermc; mod run; use std::{path::PathBuf, str::FromStr}; @@ -72,6 +73,9 @@ pub enum Commands { Run(RunCli), /// Interact with the backup system without starting a server Backup(BackupArgs), + /// Interact with the PaperMC API and download new JARs + #[command(name = "papermc")] + PaperMC(papermc::Cli), } impl Cli { @@ -81,6 +85,7 @@ impl Cli { match &self.command { Commands::Run(args) => args.run(self, &config), Commands::Backup(args) => Ok(args.run(&config)?), + Commands::PaperMC(cli) => cli.run(), } } diff --git a/alex/src/cli/papermc.rs b/alex/src/cli/papermc.rs new file mode 100644 index 0000000..4bc9216 --- /dev/null +++ b/alex/src/cli/papermc.rs @@ -0,0 +1,210 @@ +use std::path::{Path, PathBuf}; + +use chrono::Local; +use clap::{Args, Subcommand}; + +#[derive(Args)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Show information or a specific version or build + Show(ShowArgs), + /// List all versions PaperMC jars are available for + ListVersions, + /// List all available builds for a given version + ListBuilds(ListBuildsArgs), + /// Download the jar for a specific build + Download(DownloadBuildArgs), +} + +#[derive(Args)] +pub struct ShowArgs { + /// Version to show information for + version: String, + + /// Build within version to show information for + build: Option, +} + +#[derive(Args)] +pub struct ListBuildsArgs { + /// Version to list builds for + version: String, +} + +#[derive(Args)] +pub struct DownloadBuildArgs { + /// Version of build to download + version: String, + /// Build number for build to download + build: String, + + /// Path to store the new JAR file in; stores JAR in the local directory if not specified + #[arg(short, long, value_name = "OUT_PATH")] + out: Option, +} + +impl Cli { + pub fn run(&self) -> crate::Result<()> { + match &self.command { + Commands::Show(ShowArgs { + version, + build: None, + }) => show_version(&version), + Commands::Show(ShowArgs { + version, + build: Some(build), + }) => show_build(&version, &build), + Commands::ListVersions => print_versions(), + Commands::ListBuilds(args) => print_builds(&args.version), + Commands::Download(args) => { + download_build(&args.version, &args.build, args.out.as_deref()) + } + } + + Ok(()) + } +} + +fn show_version(version_str: &str) { + let client = papermc_api::Client::new(); + + let version = match client.project("paper").version(version_str).info() { + Ok(version) => version, + Err(err) => { + println!("failed to query API: {err}"); + return; + } + }; + + println!("id: {}", version.id); + println!("status: {}", version.support_status); + println!("Min. Java: {}", version.java.minimum_version); + println!("builds: {}", version.builds.len()); +} + +fn show_build(version_str: &str, build_str: &str) { + let client = papermc_api::Client::new(); + + let build = match client + .project("paper") + .version(version_str) + .build(build_str) + { + Ok(build) => build, + Err(err) => { + println!("failed to query API: {err}"); + return; + } + }; + + println!("id: {}", build.id); + println!("time: {}", build.time.with_timezone(&Local)); + println!("channel: {}", build.channel); + println!("commits:"); + + for commit in build.commits { + println!("- SHA: {}", commit.sha); + println!(" time: {}", commit.time.with_timezone(&Local)); + println!(" message: {}", commit.message); + } + + println!("downloads:"); + + for (name, download) in build.downloads.iter() { + println!(" {name}:"); + println!(" name: {}", download.name); + println!(" size: {}", download.size); + println!(" URL: {}", download.url); + println!(" checksums:"); + + for (name, value) in download.checksums.iter() { + println!(" - {}: {}", name, value); + } + } +} + +fn print_versions() { + let client = papermc_api::Client::new(); + + match client.project("paper").versions() { + Ok(versions) => { + for version in versions { + println!("{}", version.id); + } + } + Err(err) => { + println!("failed to query API: {err}"); + } + } +} + +fn print_builds(version: &str) { + let client = papermc_api::Client::new(); + + match client.project("paper").version(version).builds() { + Ok(builds) => { + for build in builds { + println!("{} ({})", build.id, build.channel); + } + } + Err(err) => { + println!("failed to query API: {err}"); + } + } +} + +fn download_build(version: &str, build: &str, out_path: Option<&Path>) { + let client = papermc_api::Client::new(); + let build = match client.project("paper").version(version).build(build) { + Ok(build) => build, + Err(err) => { + println!("failed to query API: {err}"); + return; + } + }; + + let filename = format!("paper-{}-{}.jar", version, build.id); + let dest_path = match out_path { + Some(path) if path.is_dir() => path.join(filename), + Some(path) => path.to_path_buf(), + None => PathBuf::from(filename), + }; + + let download_url = match build + .downloads + .get("server:default") + .or(build.downloads.values().next()) + { + Some(download) => &download.url, + None => { + println!("no download URLs found for build."); + return; + } + }; + + let mut f = match std::fs::File::create(dest_path) { + Ok(f) => f, + Err(err) => { + println!("failed to create destination file: {err}"); + return; + } + }; + + let mut res = match ureq::get(download_url).call() { + Ok(res) => res, + Err(err) => { + println!("failed to download file: {err}"); + return; + } + }; + + let mut reader = res.body_mut().as_reader(); + if let Err(err) = std::io::copy(&mut reader, &mut f) { + println!("failed to download file: {err}"); + } +} diff --git a/alex/src/error.rs b/alex/src/error.rs index 0ea7532..b46a49e 100644 --- a/alex/src/error.rs +++ b/alex/src/error.rs @@ -6,6 +6,7 @@ pub type Result = std::result::Result; pub enum Error { IO(io::Error), Figment(figment::Error), + Other(Box), } impl fmt::Display for Error { @@ -13,6 +14,7 @@ impl fmt::Display for Error { match self { Error::IO(err) => write!(fmt, "{}", err), Error::Figment(err) => write!(fmt, "{}", err), + Error::Other(err) => write!(fmt, "{}", err), } } }