From a6e2feedb6192815952da486199f45633ea64b32 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Sat, 20 Jul 2024 09:50:37 +0000 Subject: [PATCH] work --- Cargo.lock | 50 ++++++++++++++++++++++--- Cargo.toml | 2 + src/{ => _archive}/app/actions.rs | 0 src/{ => _archive}/app/caching.rs | 0 src/{ => _archive}/app/downloading.rs | 0 src/{ => _archive}/app/feedback.rs | 0 src/{ => _archive}/app/from_string.rs | 0 src/{ => _archive}/app/hashing.rs | 0 src/{ => _archive}/app/mod.rs | 0 src/{ => _archive}/app/progress.rs | 0 src/{ => _archive}/app/resolvable.rs | 0 src/{ => _archive}/app/steps.rs | 0 src/api/app/actions/build/addons.rs | 46 ++++++++++++++++++++--- src/api/app/actions/build/bootstrap.rs | 12 ++++-- src/api/app/actions/build/mod.rs | 1 + src/api/app/actions/build/server_jar.rs | 1 + src/api/app/actions/script/mod.rs | 4 +- src/api/app/cache/mod.rs | 6 ++- src/api/app/http.rs | 4 +- src/api/app/logging/mod.rs | 11 ++++++ src/api/app/mod.rs | 2 + src/api/app/options/mod.rs | 2 + src/api/app/step/mod.rs | 8 ++++ src/api/app/step/remove_file.rs | 17 +++++++++ src/api/models/addon/addon_type.rs | 3 +- src/api/models/launcher/mod.rs | 3 +- src/api/models/launcher/preset_flags.rs | 3 +- src/api/models/markdown/mod.rs | 35 +++++++++-------- src/api/models/modpack_source.rs | 5 ++- src/api/models/server/mod.rs | 7 ++-- src/api/models/server/server_flavor.rs | 3 +- src/api/models/server/server_type.rs | 9 +++-- src/api/models/source.rs | 5 ++- src/api/sources/mcman_meta/mod.rs | 25 +++++++++++++ src/api/sources/mod.rs | 1 + src/api/step/mod.rs | 14 +++++-- src/api/tools/git/mod.rs | 20 ++++++++++ src/api/tools/mod.rs | 1 + src/api/utils/accessor.rs | 15 ++++---- src/api/utils/fs.rs | 11 +++++- src/api/utils/logger/mod.rs | 7 ++++ src/api/utils/mod.rs | 1 + src/api/utils/zip.rs | 4 +- src/api/ws/mod.rs | 2 + src/main.rs | 3 +- 45 files changed, 281 insertions(+), 62 deletions(-) rename src/{ => _archive}/app/actions.rs (100%) rename src/{ => _archive}/app/caching.rs (100%) rename src/{ => _archive}/app/downloading.rs (100%) rename src/{ => _archive}/app/feedback.rs (100%) rename src/{ => _archive}/app/from_string.rs (100%) rename src/{ => _archive}/app/hashing.rs (100%) rename src/{ => _archive}/app/mod.rs (100%) rename src/{ => _archive}/app/progress.rs (100%) rename src/{ => _archive}/app/resolvable.rs (100%) rename src/{ => _archive}/app/steps.rs (100%) create mode 100644 src/api/app/logging/mod.rs create mode 100644 src/api/app/step/remove_file.rs create mode 100644 src/api/sources/mcman_meta/mod.rs create mode 100644 src/api/tools/git/mod.rs create mode 100644 src/api/utils/logger/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 49f6952..8f86c6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,23 +57,24 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "anstream" -version = "0.6.8" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628a8f9bd1e24b4e0db2b4bc2d000b001e7dd032d54afa60a68836aeec5aa54a" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" @@ -607,6 +608,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -922,6 +946,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -1049,6 +1079,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.12.1" @@ -1152,9 +1188,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" @@ -1179,6 +1215,7 @@ dependencies = [ "dialoguer", "digest", "dirs", + "env_logger", "futures", "glob", "hex", @@ -1186,6 +1223,7 @@ dependencies = [ "indextree", "indicatif", "lazy_static", + "log", "md-5", "murmur2", "notify-debouncer-full", diff --git a/Cargo.toml b/Cargo.toml index 1daa407..562c76c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,3 +69,5 @@ indextree = "4.6" schemars = "0.8" ratatui = "0.27.0" tokio-tungstenite = "0.23.1" +log = "0.4.22" +env_logger = "0.11.3" diff --git a/src/app/actions.rs b/src/_archive/app/actions.rs similarity index 100% rename from src/app/actions.rs rename to src/_archive/app/actions.rs diff --git a/src/app/caching.rs b/src/_archive/app/caching.rs similarity index 100% rename from src/app/caching.rs rename to src/_archive/app/caching.rs diff --git a/src/app/downloading.rs b/src/_archive/app/downloading.rs similarity index 100% rename from src/app/downloading.rs rename to src/_archive/app/downloading.rs diff --git a/src/app/feedback.rs b/src/_archive/app/feedback.rs similarity index 100% rename from src/app/feedback.rs rename to src/_archive/app/feedback.rs diff --git a/src/app/from_string.rs b/src/_archive/app/from_string.rs similarity index 100% rename from src/app/from_string.rs rename to src/_archive/app/from_string.rs diff --git a/src/app/hashing.rs b/src/_archive/app/hashing.rs similarity index 100% rename from src/app/hashing.rs rename to src/_archive/app/hashing.rs diff --git a/src/app/mod.rs b/src/_archive/app/mod.rs similarity index 100% rename from src/app/mod.rs rename to src/_archive/app/mod.rs diff --git a/src/app/progress.rs b/src/_archive/app/progress.rs similarity index 100% rename from src/app/progress.rs rename to src/_archive/app/progress.rs diff --git a/src/app/resolvable.rs b/src/_archive/app/resolvable.rs similarity index 100% rename from src/app/resolvable.rs rename to src/_archive/app/resolvable.rs diff --git a/src/app/steps.rs b/src/_archive/app/steps.rs similarity index 100% rename from src/app/steps.rs rename to src/_archive/app/steps.rs diff --git a/src/api/app/actions/build/addons.rs b/src/api/app/actions/build/addons.rs index 04dc301..8e0313c 100644 --- a/src/api/app/actions/build/addons.rs +++ b/src/api/app/actions/build/addons.rs @@ -1,19 +1,34 @@ -use std::{path::Path, sync::Arc}; +use std::{collections::HashSet, path::Path, sync::Arc}; use anyhow::{Context, Result}; use futures::{stream, StreamExt, TryStreamExt}; -use crate::api::{app::App, models::Addon}; +use crate::api::{app::App, models::Addon, step::Step}; impl App { + /// Installs new addons and removes old removed addons pub async fn action_install_addons(self: Arc, base: &Path) -> Result<()> { let addons = self.collect_addons().await?; let base = Arc::new(base.to_owned()); - const MAX_CONCURRENT_TASKS: usize = 20; + let (addons_to_add, addons_to_remove): (Vec, Vec) = if let Some(lockfile) = &*self.existing_lockfile.read().await { + let mut old = HashSet::new(); + old.extend(lockfile.addons.clone()); - stream::iter(addons).map(Ok).try_for_each_concurrent( - Some(MAX_CONCURRENT_TASKS), + let mut new = HashSet::new(); + new.extend(addons); + + (new.difference(&old).map(ToOwned::to_owned).collect(), old.difference(&new).map(ToOwned::to_owned).collect()) + } else { + (addons, vec![]) + }; + + for addon in &addons_to_remove { + self.clone().action_remove_addon(&base, addon).await?; + } + + stream::iter(addons_to_add).map(Ok).try_for_each_concurrent( + Some(20), move |addon| { let app = self.clone(); let base = base.clone(); @@ -27,10 +42,31 @@ impl App { Ok(()) } + /// Installs a single addon pub async fn action_install_addon(self: Arc, base: &Path, addon: &Addon) -> Result<()> { let steps = addon.resolve_steps(&self).await?; let dir = base.join(addon.target.as_str()); self.execute_steps(&dir, &steps).await?; Ok(()) } + + /// Removes a single addon + pub async fn action_remove_addon(self: Arc, base: &Path, addon: &Addon) -> Result<()> { + let steps = addon.resolve_steps(&self).await?; + let dir = base.join(addon.target.as_str()); + + // TODO + + if let Some(meta) = steps.iter().find_map(|x| match x { + Step::CacheCheck(meta) => Some(meta), + Step::Download { metadata, .. } => Some(metadata), + _ => None, + }) { + tokio::fs::remove_file(dir.join(&meta.filename)).await?; + } else { + log::error!("Couldn't remove addon: {addon:#?}"); + } + + Ok(()) + } } diff --git a/src/api/app/actions/build/bootstrap.rs b/src/api/app/actions/build/bootstrap.rs index b45bc18..70afa4f 100644 --- a/src/api/app/actions/build/bootstrap.rs +++ b/src/api/app/actions/build/bootstrap.rs @@ -7,6 +7,8 @@ use walkdir::WalkDir; use crate::api::{app::App, utils::{fs::create_parents, pathdiff::DiffTo}}; impl App { + /// Collects a list of paths to boostrap from + /// and calls [`Self::action_bootstrap_recursive`] for each folder root pub async fn action_bootstrap(self: Arc, base: &Path) -> Result<()> { let mut list = vec![]; @@ -23,12 +25,13 @@ impl App { Ok(()) } + /// Recursively walks through the directory and bootstraps every eccountered file pub async fn action_bootstrap_recursive(self: Arc, output_base: &Path, input_base: &Path) -> Result<()> { let output_base = Arc::new(output_base); let input_base = Arc::new(input_base); if !input_base.exists() { - println!("{input_base:?} doesnt exist"); + log::warn!("{input_base:?} doesnt exist"); return Ok(()); } @@ -58,6 +61,7 @@ impl App { Ok(()) } + /// Decides if file should be 'bootstrapped' (uses variables etc.) or not (straight up copy file) pub async fn should_bootstrap_file(&self, file: &Path) -> bool { let ext = file.extension().unwrap_or_default().to_str().unwrap_or_default(); @@ -77,6 +81,8 @@ impl App { bootstrap_exts.contains(&ext) } + /// Process a single file. Calls [`Self::should_bootstrap_file`] on the file to decide if it should process it + /// or copy it. pub async fn action_bootstrap_file(self: Arc, output_base: &Path, input_base: &Path, file: &Path) -> Result<()> { let lockfile_entry = self.existing_lockfile.read().await.as_ref().map(|lock| lock.bootstrapped_files.get(file)).flatten().cloned(); @@ -96,13 +102,13 @@ impl App { tokio::fs::write(&output_path, bootstrapped_contents.as_ref()).await .with_context(|| format!("Writing to {output_path:?}"))?; - println!("Bootstrapped: {relative:?}"); + log::info!("Bootstrapped: {relative:?}"); } else { create_parents(&output_path).await?; tokio::fs::copy(file, &output_path).await .with_context(|| format!("Copying {file:?} to {output_path:?}"))?; - println!("Copied: {relative:?}"); + log::info!("Copied: {relative:?}"); } diff --git a/src/api/app/actions/build/mod.rs b/src/api/app/actions/build/mod.rs index d2eece6..a486c28 100644 --- a/src/api/app/actions/build/mod.rs +++ b/src/api/app/actions/build/mod.rs @@ -10,6 +10,7 @@ pub mod worlds; pub mod bootstrap; impl App { + /// Builds the entire server pub async fn action_build(self: Arc, base: &Path) -> Result<()> { self.action_install_jar(base).await?; self.clone().action_install_addons(base).await?; diff --git a/src/api/app/actions/build/server_jar.rs b/src/api/app/actions/build/server_jar.rs index 8bbff6d..bc0900d 100644 --- a/src/api/app/actions/build/server_jar.rs +++ b/src/api/app/actions/build/server_jar.rs @@ -5,6 +5,7 @@ use anyhow::Result; use crate::api::{app::App, models::Environment}; impl App { + /// Installs the server jar according to [`crate::api::models::server::Server::jar`] pub async fn action_install_jar(&self, base: &Path) -> Result<()> { if let Some(jar) = self.server.read().await.as_ref().map(|(_, server)| { server.jar.clone() diff --git a/src/api/app/actions/script/mod.rs b/src/api/app/actions/script/mod.rs index d1ae447..71928ad 100644 --- a/src/api/app/actions/script/mod.rs +++ b/src/api/app/actions/script/mod.rs @@ -2,11 +2,13 @@ use std::path::Path; use anyhow::Result; -use crate::api::{app::App, models::server::Server, tools::java::{get_java_installation_for, JavaInstallation}, utils::script::Shell}; +use crate::api::{app::{App, APP_VERSION}, models::server::Server, tools::java::get_java_installation_for, utils::script::Shell}; impl App { async fn get_script_lines_for(&self, shell: &Shell, server: &Server) -> Result> { let mut lines = vec![]; + + lines.push(shell.comment(&format!("generated by mcman/{APP_VERSION}"))); if *shell == Shell::Bat { lines.push(format!("title {}", server.name)); diff --git a/src/api/app/cache/mod.rs b/src/api/app/cache/mod.rs index 681b999..23e1999 100644 --- a/src/api/app/cache/mod.rs +++ b/src/api/app/cache/mod.rs @@ -4,10 +4,10 @@ use std::{ path::PathBuf, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use serde::de::DeserializeOwned; -use crate::api::{step::CacheLocation, utils::fs::{create_parents, create_parents_sync}}; +use crate::api::{step::CacheLocation, utils::fs::create_parents_sync}; pub struct Cache(pub Option); @@ -25,6 +25,7 @@ impl Cache { Some(self.0.as_ref()?.join(path)) } + /// Tries to read a json file from cache. Returns `None` if it's not in cache or there is not a cache folder pub fn try_get_json(&self, path: &str) -> Result> { match &self.0 { Some(base) => { @@ -44,6 +45,7 @@ impl Cache { } } + /// Try to write a json file to cache. Does nothing if there is no cache folder pub fn try_write_json(&self, path: &str, data: &T) -> Result<()> { match &self.0 { Some(base) => { diff --git a/src/api/app/http.rs b/src/api/app/http.rs index 1e16bd9..49e0d5c 100644 --- a/src/api/app/http.rs +++ b/src/api/app/http.rs @@ -13,7 +13,7 @@ impl App { url: impl IntoUrl, f: F, ) -> Result { - println!("HTTP GET {}", url.as_str()); + log::trace!("GET {}", url.as_str()); let req = self.http_client.get(url.as_str()); @@ -26,6 +26,8 @@ impl App { .get("x-ratelimit-remaining") .is_some_and(|x| String::from_utf8_lossy(x.as_bytes()) == "1") { + log::info!("Hit ratelimit"); + let ratelimit_reset = String::from_utf8_lossy(res.headers()["x-ratelimit-reset"].as_bytes()) .parse::()?; diff --git a/src/api/app/logging/mod.rs b/src/api/app/logging/mod.rs new file mode 100644 index 0000000..cd99e40 --- /dev/null +++ b/src/api/app/logging/mod.rs @@ -0,0 +1,11 @@ +use std::path::PathBuf; + +pub enum Message { + File(FileMessage, PathBuf), +} + +pub enum FileMessage { + Copied, + Bootstrapped, + CacheHit, +} diff --git a/src/api/app/mod.rs b/src/api/app/mod.rs index 78c7ca2..dc1d330 100644 --- a/src/api/app/mod.rs +++ b/src/api/app/mod.rs @@ -16,6 +16,8 @@ mod io; pub mod options; mod step; mod vars; +mod logging; +pub use logging::*; pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/api/app/options/mod.rs b/src/api/app/options/mod.rs index 22a2ab5..0bdee1e 100644 --- a/src/api/app/options/mod.rs +++ b/src/api/app/options/mod.rs @@ -35,4 +35,6 @@ pub struct ApiUrls { pub mclogs: String, #[config(default = "https://api.papermc.io/v2", env = "API_URL_MCLOGS")] pub papermc: String, + #[config(default = "https://raw.githubusercontent.com/ParadigmMC/mcman-meta/main", env = "API_URL_MCMAN_META")] + pub mcman_meta: String, } diff --git a/src/api/app/step/mod.rs b/src/api/app/step/mod.rs index 112e2d7..710ef1d 100644 --- a/src/api/app/step/mod.rs +++ b/src/api/app/step/mod.rs @@ -7,10 +7,13 @@ use crate::api::step::{Step, StepResult}; mod cache_check; mod download; mod execute_java; +mod remove_file; use super::App; impl App { + /// Execute a list of steps, taking care of their StepResult's. + /// Skips the next step when a step returns StepResult::Skip pub async fn execute_steps(&self, dir: &Path, steps: &[Step]) -> Result<()> { let mut iter = steps.iter(); @@ -24,6 +27,7 @@ impl App { Ok(()) } + /// Execute a single step and return its result pub async fn execute_step(&self, dir: &Path, step: &Step) -> Result { match step { Step::CacheCheck(metadata) => self.execute_step_cache_check(dir, metadata).await @@ -43,6 +47,10 @@ impl App { } => { self.execute_step_execute_java(dir, args, *java_version, label).await }, + + Step::RemoveFile(metadata) => { + self.execute_step_remove_file(dir, metadata).await + }, } } } diff --git a/src/api/app/step/remove_file.rs b/src/api/app/step/remove_file.rs new file mode 100644 index 0000000..e567707 --- /dev/null +++ b/src/api/app/step/remove_file.rs @@ -0,0 +1,17 @@ +use std::path::Path; + +use anyhow::{anyhow, bail, Result}; + +use crate::api::{app::App, step::{FileMeta, StepResult}, tools::{self, java::{JavaProcess, JavaVersion}}}; + +impl App { + pub(super) async fn execute_step_remove_file( + &self, + dir: &Path, + metadata: &FileMeta, + ) -> Result { + + + Ok(StepResult::Continue) + } +} diff --git a/src/api/models/addon/addon_type.rs b/src/api/models/addon/addon_type.rs index ee3f862..c7cc735 100644 --- a/src/api/models/addon/addon_type.rs +++ b/src/api/models/addon/addon_type.rs @@ -1,6 +1,7 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, JsonSchema)] #[serde(tag = "type", rename_all = "lowercase")] #[non_exhaustive] pub enum AddonType { diff --git a/src/api/models/launcher/mod.rs b/src/api/models/launcher/mod.rs index 92d9d4f..fca1a6e 100644 --- a/src/api/models/launcher/mod.rs +++ b/src/api/models/launcher/mod.rs @@ -1,3 +1,4 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{borrow::ToOwned, collections::HashMap, env}; @@ -6,7 +7,7 @@ use crate::api::utils::serde::*; mod preset_flags; pub use preset_flags::*; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, JsonSchema)] #[serde(default)] pub struct ServerLauncher { pub eula_args: bool, diff --git a/src/api/models/launcher/preset_flags.rs b/src/api/models/launcher/preset_flags.rs index 62ec5d6..ffd1087 100644 --- a/src/api/models/launcher/preset_flags.rs +++ b/src/api/models/launcher/preset_flags.rs @@ -1,6 +1,7 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum PresetFlags { Aikars, diff --git a/src/api/models/markdown/mod.rs b/src/api/models/markdown/mod.rs index 12f4d1c..2652c7b 100644 --- a/src/api/models/markdown/mod.rs +++ b/src/api/models/markdown/mod.rs @@ -1,43 +1,48 @@ use std::collections::HashMap; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::api::utils::serde::*; pub mod render; -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, JsonSchema)] pub enum MarkdownOutput { #[default] ASCII, HTML, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] pub struct MarkdownOptions { pub files: Vec, - #[serde(skip_serializing_if = "is_default")] + #[serde(skip_serializing_if = "is_default", default = "default_columns")] pub columns: Vec, - #[serde(skip_serializing_if = "is_default")] + #[serde(skip_serializing_if = "is_default", default)] pub titles: HashMap, - #[serde(skip_serializing_if = "is_true")] + #[serde(skip_serializing_if = "is_true", default = "bool_true")] pub name_includes_link: bool, - #[serde(skip_serializing_if = "is_default")] + #[serde(skip_serializing_if = "is_default", default)] pub sort_by: MdSort, - #[serde(skip_serializing_if = "is_default")] + #[serde(skip_serializing_if = "is_default", default)] pub output_type: MarkdownOutput, } +fn default_columns() -> Vec { + vec![ + MdColumn::Icon, + MdColumn::Name, + MdColumn::Description, + MdColumn::Version, + ] +} + impl Default for MarkdownOptions { fn default() -> Self { Self { files: vec![String::from("README.md")], - columns: vec![ - MdColumn::Icon, - MdColumn::Name, - MdColumn::Description, - MdColumn::Version, - ], + columns: default_columns(), name_includes_link: true, titles: HashMap::new(), sort_by: MdSort::default(), @@ -46,7 +51,7 @@ impl Default for MarkdownOptions { } } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum MdColumn { Icon, @@ -69,7 +74,7 @@ impl ToString for MdColumn { } -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum MdSort { Alphabetical, diff --git a/src/api/models/modpack_source.rs b/src/api/models/modpack_source.rs index 500e6fb..96e463a 100644 --- a/src/api/models/modpack_source.rs +++ b/src/api/models/modpack_source.rs @@ -1,9 +1,10 @@ use anyhow::Result; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::api::utils::accessor::Accessor; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(untagged)] pub enum ModpackSource { Local { @@ -35,7 +36,7 @@ impl ModpackSource { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum ModpackType { Packwiz, diff --git a/src/api/models/server/mod.rs b/src/api/models/server/mod.rs index cb3c0be..c2a3993 100644 --- a/src/api/models/server/mod.rs +++ b/src/api/models/server/mod.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::{launcher::ServerLauncher, markdown::MarkdownOptions, Source}; @@ -12,7 +13,7 @@ pub use server_type::*; pub const SERVER_TOML: &str = "server.toml"; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, JsonSchema)] #[serde(default)] pub struct Server { pub name: String, @@ -20,10 +21,10 @@ pub struct Server { pub jar: Option, - #[serde(default = "Vec::new")] + #[serde(default = "Vec::::new")] pub sources: Vec, - #[serde(default = "HashMap::new")] + #[serde(default = "HashMap::::new")] pub variables: HashMap, #[serde(default)] diff --git a/src/api/models/server/server_flavor.rs b/src/api/models/server/server_flavor.rs index 757eca4..03706b5 100644 --- a/src/api/models/server/server_flavor.rs +++ b/src/api/models/server/server_flavor.rs @@ -1,6 +1,7 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Default)] +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Default, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum ServerFlavor { #[default] diff --git a/src/api/models/server/server_type.rs b/src/api/models/server/server_type.rs index 163ebb3..481323f 100644 --- a/src/api/models/server/server_type.rs +++ b/src/api/models/server/server_type.rs @@ -2,11 +2,12 @@ use crate::api::{ app::App, models::{Addon, AddonTarget, AddonType, Environment}, sources::buildtools, step::Step, utils::serde::* }; use anyhow::Result; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::ServerFlavor; -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum PaperMCProject { #[default] @@ -28,14 +29,14 @@ impl ToString for PaperMCProject { } } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] pub struct ServerJar { pub mc_version: String, #[serde(flatten)] pub server_type: ServerType, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ServerType { Vanilla {}, @@ -81,7 +82,7 @@ pub enum ServerType { #[serde(default)] craftbukkit: bool, #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde(default = "Vec::new")] + #[serde(default = "Vec::::new")] args: Vec, }, diff --git a/src/api/models/source.rs b/src/api/models/source.rs index 42385c3..7cec145 100644 --- a/src/api/models/source.rs +++ b/src/api/models/source.rs @@ -1,18 +1,19 @@ use std::path::PathBuf; use anyhow::{Context, Result}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::{mrpack::resolve_mrpack_addons, packwiz::resolve_packwiz_addons, Addon, AddonListFile, ModpackType}; use crate::api::{app::App, models::ModpackSource, utils::{accessor::Accessor, toml::read_toml}}; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct Source { #[serde(flatten)] pub source_type: SourceType, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(tag = "type", rename_all = "lowercase")] pub enum SourceType { File { diff --git a/src/api/sources/mcman_meta/mod.rs b/src/api/sources/mcman_meta/mod.rs new file mode 100644 index 0000000..5860fbe --- /dev/null +++ b/src/api/sources/mcman_meta/mod.rs @@ -0,0 +1,25 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; + +use crate::api::app::App; + +pub struct McmanMetaAPI<'a>(pub &'a App); + +impl<'a> McmanMetaAPI<'a> { + pub async fn get(&self, folder: &str, path: &str) -> Result { + Ok(self.0.http_get(format!("{}/{folder}/{path}", self.0.options.api_urls.mcman_meta)) + .await? + .text() + .await?) + } + + pub async fn ls(&self, folder: &str) -> Result> { + Ok( + self.get(folder, "ls") + .await? + .split('\n') + .map(ToOwned::to_owned) + .collect() + ) + } +} diff --git a/src/api/sources/mod.rs b/src/api/sources/mod.rs index 96c6f15..8192431 100644 --- a/src/api/sources/mod.rs +++ b/src/api/sources/mod.rs @@ -12,3 +12,4 @@ pub mod jenkins; pub mod spigot; pub mod buildtools; pub mod quilt; +pub mod mcman_meta; diff --git a/src/api/step/mod.rs b/src/api/step/mod.rs index 4e3c632..3cc07e4 100644 --- a/src/api/step/mod.rs +++ b/src/api/step/mod.rs @@ -8,24 +8,32 @@ pub use cache_location::*; use super::tools::java::JavaVersion; +/// A step is the building block of doing things #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Step { + /// Check the cache directory for a file; + /// Copy over the file if it's in cache and skip the next step (which is a `Step::Download` in most cases) CacheCheck(FileMeta), + /// Download a file from an URL. + /// If `metadata.cache` is Some, download to cache directory and copy the file from there Download { url: String, metadata: FileMeta }, + /// Execute a java program ExecuteJava { args: Vec, java_version: Option, label: String, }, + /// Remove/delete a file + RemoveFile(FileMeta), } +/// Result of executing a step #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum StepResult { - // continue into running next step + /// Continue executing to next step #[default] Continue, - // skip next steps for this addon - // example: cache hit + /// Skip the next step Skip, } diff --git a/src/api/tools/git/mod.rs b/src/api/tools/git/mod.rs new file mode 100644 index 0000000..06c004a --- /dev/null +++ b/src/api/tools/git/mod.rs @@ -0,0 +1,20 @@ +use std::{ffi::OsStr, process::Command}; + +use anyhow::Result; + +pub const GIT: &str = "git"; + +pub fn git_command, S: AsRef>(args: I) -> Result { + Ok(String::from_utf8_lossy(Command::new(GIT) + .args(args) + .output()? + .stdout + .as_slice() + ).into_owned()) +} + +pub fn git_check() -> Option { + git_command(["--version"]).ok().map(|s| s.replacen("git version ", "", 1)) +} + + diff --git a/src/api/tools/mod.rs b/src/api/tools/mod.rs index b8d0181..62c487d 100644 --- a/src/api/tools/mod.rs +++ b/src/api/tools/mod.rs @@ -1 +1,2 @@ pub mod java; +pub mod git; diff --git a/src/api/utils/accessor.rs b/src/api/utils/accessor.rs index 9b4f240..20af89d 100644 --- a/src/api/utils/accessor.rs +++ b/src/api/utils/accessor.rs @@ -1,7 +1,4 @@ -use std::{ - io::{Read, Seek}, - path::PathBuf, -}; +use std::path::PathBuf; use anyhow::{anyhow, Result}; use reqwest::Url; @@ -10,13 +7,12 @@ use zip::ZipArchive; use crate::api::app::App; -//pub trait ReadSeek: Read + Seek {} - +/// An `Accessor` allows for filesystem, remote or zip file access. pub enum Accessor { Local(PathBuf), Remote(reqwest::Url), ZipLocal(ZipArchive), - //Zip(ZipArchive>), + //ZipRemote(SomeSortOfTempFile, ZipArchive), } impl ToString for Accessor { @@ -24,7 +20,7 @@ impl ToString for Accessor { match self { Accessor::Local(path) => path.to_string_lossy().into_owned(), Accessor::Remote(url) => url.to_string(), - Accessor::ZipLocal(zip) => String::from("a zip archive"), + Accessor::ZipLocal(_) => String::from("a zip archive"), } } } @@ -42,6 +38,7 @@ impl Accessor { } } + /// Try listing a directory pub async fn dir(&self) -> Result> { match self { Accessor::ZipLocal(zip) => Ok(zip.file_names().map(ToOwned::to_owned).collect()), @@ -54,6 +51,7 @@ impl Accessor { } } + /// Read a JSON file pub async fn json(&mut self, app: &App, path: &str) -> Result { match self { Accessor::Local(base) => Ok(serde_json::from_reader(std::fs::File::open( @@ -64,6 +62,7 @@ impl Accessor { } } + /// Read a TOML file pub async fn toml(&mut self, app: &App, path: &str) -> Result { match self { Accessor::Local(base) => Ok(toml::from_str( diff --git a/src/api/utils/fs.rs b/src/api/utils/fs.rs index b01be45..b5c8647 100644 --- a/src/api/utils/fs.rs +++ b/src/api/utils/fs.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{ffi::OsStr, path::{Path, PathBuf}}; use anyhow::{anyhow, Context, Result}; @@ -14,3 +14,12 @@ pub fn create_parents_sync(path: &Path) -> Result<()> { std::fs::create_dir_all(parent) .with_context(|| format!("Creating directory: {parent:?}")) } + +pub fn some_if_exists>(path: &T) -> Option { + let v = PathBuf::from(path); + if v.exists() { + Some(v) + } else { + None + } +} diff --git a/src/api/utils/logger/mod.rs b/src/api/utils/logger/mod.rs new file mode 100644 index 0000000..33e87d5 --- /dev/null +++ b/src/api/utils/logger/mod.rs @@ -0,0 +1,7 @@ +use anyhow::Result; + +pub fn init_logger() -> Result<()> { + env_logger::init(); + + Ok(()) +} diff --git a/src/api/utils/mod.rs b/src/api/utils/mod.rs index 10997b4..fda8ff7 100644 --- a/src/api/utils/mod.rs +++ b/src/api/utils/mod.rs @@ -9,3 +9,4 @@ pub mod script; pub mod zip; pub mod toml; pub mod fs; +pub mod logger; diff --git a/src/api/utils/zip.rs b/src/api/utils/zip.rs index 32a00e2..f63ff0d 100644 --- a/src/api/utils/zip.rs +++ b/src/api/utils/zip.rs @@ -1,6 +1,6 @@ use std::{io::{Read, Seek, Write}, path::Path}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use walkdir::WalkDir; use zip::{write::FileOptions, ZipArchive, ZipWriter}; @@ -8,6 +8,7 @@ use crate::api::app::APP_VERSION; use super::{fs::create_parents_sync, pathdiff::DiffTo}; +/// Unzip a zip archive to somewhere on the file system pub async fn unzip(reader: T, to: &Path, prefix: Option) -> Result<()> { let mut archive = ZipArchive::new(reader)?; @@ -40,6 +41,7 @@ pub async fn unzip(reader: T, to: &Path, prefix: Option) Ok(()) } +/// Zip a folder from the filesystem into a writer pub async fn zip(writer: T, folder: &Path) -> Result<()> { let mut archive = ZipWriter::new(writer); diff --git a/src/api/ws/mod.rs b/src/api/ws/mod.rs index e71edf6..03a9466 100644 --- a/src/api/ws/mod.rs +++ b/src/api/ws/mod.rs @@ -1,3 +1,5 @@ +//! WebSocket Server implementation for third party editors + use std::sync::Arc; use anyhow::{Context, Result}; diff --git a/src/main.rs b/src/main.rs index 3bcc86c..74c263f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use api::app::App; +use api::{app::App, utils::logger::init_logger}; use clap::Parser; mod api; @@ -33,6 +33,7 @@ enum Commands { #[tokio::main] async fn main() -> Result<()> { + init_logger(); let args = Cli::parse(); let app = Arc::new(App::new()?);