From f5b6afd79068db4745b0fb6bfb000543bdb28681 Mon Sep 17 00:00:00 2001 From: Spookdot <333.333.333@gmx.de> Date: Thu, 25 Aug 2022 11:39:54 +0200 Subject: [PATCH] Move functions into new HopperClient struct --- src/client.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 6 +-- src/main.rs | 123 +++--------------------------------------------- 3 files changed, 138 insertions(+), 119 deletions(-) create mode 100644 src/client.rs diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..018f794 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,128 @@ +use crate::api::{ModInfo, ModResult, ModVersion, ModVersionFile, SearchResponse}; +use crate::config::{Args, Config, SearchArgs}; +use futures_util::StreamExt; +use log::*; +use std::cmp::min; +use std::io::Write; + +pub struct HopperClient { + config: Config, + client: reqwest::Client, +} + +impl HopperClient { + pub fn new(config: Config) -> Self { + Self { + config: config, + client: reqwest::Client::new(), + } + } + + pub async fn search_mods(&self, search_args: &SearchArgs) -> anyhow::Result { + println!("Searching with query \"{}\"...", search_args.package_name); + + let url = format!("https://{}/v2/search", self.config.upstream.server_address); + + let mut params = vec![("query", search_args.package_name.to_owned())]; + if let Some(versions) = &search_args.version { + let versions_facets = versions + .iter() + .map(|e| format!("[\"versions:{}\"]", e)) + .collect::>() + .join(","); + params.push(("facets", format!("[{}]", versions_facets))); + } + + let url = reqwest::Url::parse_with_params(url.as_str(), ¶ms)?; + info!("GET {}", url); + let response = self + .client + .get(url) + .send() + .await? + .json::() + .await?; + Ok(response) + } + + pub async fn fetch_mod_info(&self, mod_result: &ModResult) -> anyhow::Result { + let mod_id = &mod_result.project_id; + println!( + "Fetching mod info for {} (ID: {})...", + mod_result.title, mod_id + ); + + let url = format!( + "https://{}/v2/project/{}", + self.config.upstream.server_address, mod_id + ); + info!("GET {}", url); + let response = self.client.get(url).send().await?; + let response = response.json::().await?; + Ok(response) + } + + pub async fn fetch_mod_version(&self, version_id: &String) -> anyhow::Result { + println!("Fetching mod version {}...", version_id); + + let url = format!( + "https://{}/v2/version/{}", + self.config.upstream.server_address, version_id + ); + info!("GET {}", url); + let response = self.client.get(url).send().await?; + let response = response.json::().await?; + Ok(response) + } + + pub async fn download_version_file( + &self, + args: &Args, + file: &ModVersionFile, + ) -> anyhow::Result<()> { + // TODO replace all uses of .unwrap() with proper error codes + let filename = &file.filename; + + // TODO make confirmation skippable with flag argument + if !args.auto_accept { + use dialoguer::Confirm; + let prompt = format!("Download to {}?", filename); + let confirm = Confirm::new() + .with_prompt(prompt) + .default(true) + .interact()?; + if !confirm { + println!("Skipping downloading {}...", filename); + return Ok(()); + } + } + let url = &file.url; + info!("GET {}", url); + let response = self.client.get(url).send().await?; + let total_size = response.content_length().unwrap(); + + // TODO better colors and styling! + // TODO square colored creeper face progress indicator (from top-left clockwise spiral in) + use indicatif::{ProgressBar, ProgressStyle}; + let pb = ProgressBar::new(total_size); + pb.set_style(ProgressStyle::default_bar().template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").progress_chars("#>-")); + pb.set_message(&format!("Downloading {}", url)); + + let filename = &file.filename; + let mut file = std::fs::File::create(filename)?; + let mut downloaded: u64 = 0; + let mut stream = response.bytes_stream(); + + // TODO check hashes while streaming + while let Some(item) = stream.next().await { + let chunk = &item.unwrap(); + file.write(&chunk)?; + let new = min(downloaded + (chunk.len() as u64), total_size); + downloaded = new; + pb.set_position(new); + } + + pb.finish_with_message(&format!("Downloaded {} to {}", url, filename)); + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index d715e8c..44aa5a2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -59,7 +59,7 @@ impl Args { } } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct Upstream { /// Modrinth main server address pub server_address: String, @@ -73,7 +73,7 @@ impl Default for Upstream { } } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct Options { /// Whether to reverse search results pub reverse_search: bool, @@ -87,7 +87,7 @@ impl Default for Options { } } -#[derive(Deserialize, Serialize, Debug, Default)] +#[derive(Deserialize, Serialize, Debug, Default, Clone)] pub struct Config { /// General settings pub options: Options, diff --git a/src/main.rs b/src/main.rs index e4ae0c3..b50fc8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,42 +1,13 @@ -use futures_util::StreamExt; -use log::*; -use std::cmp::min; -use std::io::Write; use structopt::StructOpt; mod api; +mod client; mod config; use api::*; +use client::*; use config::*; -async fn search_mods(ctx: &AppContext, search_args: &SearchArgs) -> anyhow::Result { - println!("Searching with query \"{}\"...", search_args.package_name); - - let client = reqwest::Client::new(); - let url = format!("https://{}/v2/search", ctx.config.upstream.server_address); - - let mut params = vec![("query", search_args.package_name.to_owned())]; - if let Some(versions) = &search_args.version { - let versions_facets = versions - .iter() - .map(|e| format!("[\"versions:{}\"]", e)) - .collect::>() - .join(","); - params.push(("facets", format!("[{}]", versions_facets))); - } - - let url = reqwest::Url::parse_with_params(url.as_str(), ¶ms)?; - info!("GET {}", url); - let response = client - .get(url) - .send() - .await? - .json::() - .await?; - Ok(response) -} - fn display_search_results(ctx: &AppContext, response: &SearchResponse) { let iter = response.hits.iter().enumerate(); if ctx.config.options.reverse_search { @@ -101,89 +72,9 @@ async fn select_from_results( Ok(selected) } -async fn fetch_mod_info(ctx: &AppContext, mod_result: &ModResult) -> anyhow::Result { - let mod_id = &mod_result.project_id; - println!( - "Fetching mod info for {} (ID: {})...", - mod_result.title, mod_id - ); - - let client = reqwest::Client::new(); - let url = format!( - "https://{}/v2/project/{}", - ctx.config.upstream.server_address, mod_id - ); - info!("GET {}", url); - let response = client.get(url).send().await?; - let response = response.json::().await?; - Ok(response) -} - -async fn fetch_mod_version(ctx: &AppContext, version_id: &String) -> anyhow::Result { - println!("Fetching mod version {}...", version_id); - - let client = reqwest::Client::new(); - let url = format!( - "https://{}/v2/version/{}", - ctx.config.upstream.server_address, version_id - ); - info!("GET {}", url); - let response = client.get(url).send().await?; - let response = response.json::().await?; - Ok(response) -} - -async fn download_version_file(ctx: &AppContext, file: &ModVersionFile) -> anyhow::Result<()> { - // TODO replace all uses of .unwrap() with proper error codes - let filename = &file.filename; - - // TODO make confirmation skippable with flag argument - if !ctx.args.auto_accept { - use dialoguer::Confirm; - let prompt = format!("Download to {}?", filename); - let confirm = Confirm::new() - .with_prompt(prompt) - .default(true) - .interact()?; - if !confirm { - println!("Skipping downloading {}...", filename); - return Ok(()); - } - } - - let client = reqwest::Client::new(); - let url = &file.url; - info!("GET {}", url); - let response = client.get(url).send().await?; - let total_size = response.content_length().unwrap(); - - // TODO better colors and styling! - // TODO square colored creeper face progress indicator (from top-left clockwise spiral in) - use indicatif::{ProgressBar, ProgressStyle}; - let pb = ProgressBar::new(total_size); - pb.set_style(ProgressStyle::default_bar().template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").progress_chars("#>-")); - pb.set_message(&format!("Downloading {}", url)); - - let filename = &file.filename; - let mut file = std::fs::File::create(filename)?; - let mut downloaded: u64 = 0; - let mut stream = response.bytes_stream(); - - // TODO check hashes while streaming - while let Some(item) = stream.next().await { - let chunk = &item.unwrap(); - file.write(&chunk)?; - let new = min(downloaded + (chunk.len() as u64), total_size); - downloaded = new; - pb.set_position(new); - } - - pb.finish_with_message(&format!("Downloaded {} to {}", url, filename)); - Ok(()) -} - async fn cmd_get(ctx: &AppContext, search_args: SearchArgs) -> anyhow::Result<()> { - let response = search_mods(ctx, &search_args).await?; + let client = HopperClient::new(ctx.config.clone()); + let response = client.search_mods(&search_args).await?; if response.hits.is_empty() { // TODO formatting @@ -202,15 +93,15 @@ async fn cmd_get(ctx: &AppContext, search_args: SearchArgs) -> anyhow::Result<() for selection in selected.iter() { let to_get = &response.hits[*selection]; - let mod_info = fetch_mod_info(ctx, to_get).await?; + let mod_info = client.fetch_mod_info(to_get).await?; // TODO allow the user to select multiple versions if let Some(version_id) = mod_info.versions.first() { println!("fetching version {}", version_id); - let version = fetch_mod_version(ctx, version_id).await?; + let version = client.fetch_mod_version(version_id).await?; for file in version.files.iter() { - download_version_file(ctx, file).await?; + client.download_version_file(&ctx.args, file).await?; } } }