From a72cac72597e7cb02d09825c3bd8752396a331a7 Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 26 Jan 2026 09:06:48 +1100 Subject: [PATCH] feat: Add name default and manual configuration --- cli/src/cli.rs | 36 +++++++++--- cli/src/commands/connect/config.rs | 70 ++++++++++------------- cli/src/commands/connect/config_option.rs | 7 ++- cli/src/commands/connect/configurable.rs | 2 +- cli/src/commands/connect/mod.rs | 2 +- cli/src/commands/connect/s3.rs | 8 ++- cli/src/commands/upload/interface.rs | 57 ++++++++++++++---- cli/src/main.rs | 7 ++- cli/src/manifest.rs | 4 +- 9 files changed, 123 insertions(+), 70 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 1ebd41e1..318fc457 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,6 +1,6 @@ use clap::{Args, Parser, Subcommand, ValueEnum}; -use crate::commands::connect::config_option::ConfigOptionCli; +use crate::{commands::connect::config_option::ConfigOptionCli, interactive_variable}; #[derive(Parser)] #[command(version, about, long_about = None)] @@ -18,27 +18,49 @@ pub enum Commands { /// Configures downpour endpoints Connect { #[arg(short, long)] - name: String, + name: Option, #[command(subcommand)] option: ConfigOptionCli, }, /// Uploads new game version to depot - Upload(UploadInfo), + Upload { + #[clap(flatten)] + info: UploadInfoCli, + #[arg(short, long)] + /// Alias of a given connection + name: Option, + }, } #[derive(Args)] pub struct UploadInfo { - /// Identifies the specific upload style that will be used for the set depot - pub upload_style: UploadStyle, + pub path: String, + pub game_id: String, + pub version_id: String, +} +#[derive(Args)] +pub struct UploadInfoCli { /// Relative path to new version files #[arg(short, long, default_value_t = String::from("."))] pub path: String, /// ID of game to attach to #[arg(short, long)] - pub game_id: String, + pub game_id: Option, /// Version ID to attach to #[arg(short, long)] - pub version_id: String, + pub version_id: Option, +} +impl UploadInfoCli { + pub fn interactive_configure(self) -> UploadInfo { + let path = self.path; + interactive_variable!(self, game_id, "Game ID"); + interactive_variable!(self, version_id, "Version ID"); + UploadInfo { + path, + game_id, + version_id, + } + } } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] diff --git a/cli/src/commands/connect/config.rs b/cli/src/commands/connect/config.rs index 1f42f7db..3a0f5ce8 100644 --- a/cli/src/commands/connect/config.rs +++ b/cli/src/commands/connect/config.rs @@ -1,18 +1,15 @@ use crate::{ - commands::{ - connect::{ - config_option::{ConfigOption, ConfigOptionCli}, - configurable::Configure, - s3::S3Config, - speedtest::{SPEEDTEST_PATH, Speedtest} - }, + commands::connect::{ + config_option::{ConfigOption, ConfigOptionCli}, + configurable::Configure, + speedtest::{SPEEDTEST_PATH, Speedtest}, }, manifest::DepotManifest, }; use dialoguer::{Confirm, theme::ColorfulTheme}; use futures::AsyncWriteExt; use indicatif::{ProgressBar, ProgressStyle}; -use log::{debug, info, warn}; +use log::{debug, info}; use opendal::Operator; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, ops::Not}; @@ -23,13 +20,13 @@ const CONFIG_DIR: &str = "downpour/config.json"; #[derive(Serialize, Deserialize)] pub struct Config { configurations: HashMap, - active_s3: Option, + active: Option, } impl Config { pub fn new() -> Self { Self { configurations: HashMap::new(), - active_s3: None, + active: None, } } pub fn exists(&self, name: &String) -> bool { @@ -48,7 +45,9 @@ impl Config { let save_path = dirs::config_dir() .expect("Apparently your home directory doesn't exist") // Should probably formalise that error .join(CONFIG_DIR); - if fs::exists(&save_path).unwrap_or_else(|_| panic!("Could not read save path {:#?}", &save_path)) { + if fs::exists(&save_path) + .unwrap_or_else(|_| panic!("Could not read save path {:#?}", &save_path)) + { serde_json::from_str(&fs::read_to_string(save_path).unwrap()).unwrap() } else { Config::new() @@ -56,50 +55,37 @@ impl Config { } pub fn add_item(&mut self, name: String, object: ConfigOption) { if matches!(object, ConfigOption::S3(..)) { - self.active_s3 = Some(name.clone()) + self.active = Some(name.clone()) } self.configurations.insert(name, object); self.save().expect("Failed to save config"); } - pub fn get_active_s3(&self) -> Option { - if let Some(active_s3) = &self.active_s3 { - self.configurations - .iter() - .filter_map(|(name, option)| { - if *name == *active_s3 { - match option { - ConfigOption::S3(s3_config) => Some(s3_config), - _ => { - warn!("Name {} is not of type 'S3'", name); - None - } - } - } else { - None - } - }) - .next() - .cloned() + pub fn get_active(&self) -> Option<&ConfigOption> { + if let Some(active) = &self.active { + self.configurations.get(active) } else { None } } - pub fn get>(&self, name: T) -> Option<&ConfigOption> { + pub fn get>(&self, name: T) -> Option<&ConfigOption> { self.configurations.get(name.as_ref()) } } pub async fn manage_configuration( config: &mut Config, - name: &String, - option: &ConfigOptionCli, + name: Option, + option: ConfigOptionCli, ) -> anyhow::Result<()> { - if config.exists(name) { + let mut name = name; + if let Some(name) = &name + && config.exists(name) + { let confirm = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(format!( "An entry already exists with the name \"{}\". Would you like to overwrite it?", - &name + name )) .interact()?; if !confirm { @@ -107,9 +93,10 @@ pub async fn manage_configuration( } } let config_option = match option { - ConfigOptionCli::S3(s3_config_cli) => s3_config_cli.clone().configure().await?, + ConfigOptionCli::S3(s3_config_cli) => s3_config_cli.clone().configure(&mut name).await?, }; - config.add_item(name.clone(), config_option.clone()); + let name = name.expect("Default name was not provided by ConfigOption. This is a bug"); + config.add_item(name, config_option.clone()); let operator = config_option.build()?; generate_manifest(&operator).await?; @@ -138,14 +125,15 @@ async fn generate_speedtest(operator: &Operator) -> anyhow::Result<()> { ); let mut reader = Speedtest::new(|progress| { - let progress_int = (progress * 100f32).round() as u64; - progress_bar.set_position(progress_int); - }); + let progress_int = (progress * 100f32).round() as u64; + progress_bar.set_position(progress_int); + }); let written = tokio::io::copy(&mut reader, &mut writer).await?; debug!("Wrote {} bytes to {:?}", written, operator.info()); writer.into_inner().close().await?; Ok(()) } + async fn generate_manifest(operator: &Operator) -> anyhow::Result<()> { let lister = operator.list_with("manifest.json").limit(1).await?; if lister.is_empty().not() { diff --git a/cli/src/commands/connect/config_option.rs b/cli/src/commands/connect/config_option.rs index 88ee6705..d0257bbd 100644 --- a/cli/src/commands/connect/config_option.rs +++ b/cli/src/commands/connect/config_option.rs @@ -2,10 +2,14 @@ use clap::Subcommand; use opendal::{Operator, layers::LoggingLayer}; use serde::{Deserialize, Serialize}; -use crate::{commands::connect::s3::{S3Config, S3ConfigCli}, operator_builder::OperatorBuilder}; +use crate::{ + commands::connect::s3::{S3Config, S3ConfigCli}, + operator_builder::OperatorBuilder, +}; #[derive(Subcommand, Clone)] pub enum ConfigOptionCli { + // Connect to any S3-compatible endpoint S3(S3ConfigCli), } #[derive(Serialize, Deserialize, Clone)] @@ -15,7 +19,6 @@ pub enum ConfigOption { impl ConfigOption { pub fn build(&self) -> anyhow::Result { - Ok(match self { ConfigOption::S3(s3_config) => s3_config.build()?, } diff --git a/cli/src/commands/connect/configurable.rs b/cli/src/commands/connect/configurable.rs index ad7b4a61..d653be30 100644 --- a/cli/src/commands/connect/configurable.rs +++ b/cli/src/commands/connect/configurable.rs @@ -1,5 +1,5 @@ use crate::commands::connect::config_option::ConfigOption; pub trait Configure { - async fn configure(self) -> anyhow::Result; + async fn configure(self, name: &mut Option) -> anyhow::Result; } diff --git a/cli/src/commands/connect/mod.rs b/cli/src/commands/connect/mod.rs index 397bc110..37f13750 100644 --- a/cli/src/commands/connect/mod.rs +++ b/cli/src/commands/connect/mod.rs @@ -4,4 +4,4 @@ pub mod s3; #[macro_use] pub mod interactive; pub mod config_option; -pub mod speedtest; \ No newline at end of file +pub mod speedtest; diff --git a/cli/src/commands/connect/s3.rs b/cli/src/commands/connect/s3.rs index e2f400ec..5b30d101 100644 --- a/cli/src/commands/connect/s3.rs +++ b/cli/src/commands/connect/s3.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; use crate::{ commands::connect::{config_option::ConfigOption, configurable::Configure}, - interactive_variable, operator_builder::OperatorBuilder, + interactive_variable, + operator_builder::OperatorBuilder, }; #[derive(Args, Clone)] @@ -28,12 +29,15 @@ pub struct S3Config { } impl Configure for S3ConfigCli { - async fn configure(self) -> anyhow::Result { + async fn configure(self, name: &mut Option) -> anyhow::Result { interactive_variable!(self, key_id, "S3 Key ID"); interactive_variable!(self, secret_key, "S3 Secret Key"); interactive_variable!(self, region, "S3 Region"); interactive_variable!(self, bucket_name, "S3 Bucket Name"); interactive_variable!(self, endpoint, "S3 Endpoint"); + if let None = name { + *name = Some(endpoint.clone()); + } Ok(ConfigOption::S3(S3Config { secret_key, key_id, diff --git a/cli/src/commands/upload/interface.rs b/cli/src/commands/upload/interface.rs index 982b7ed7..ca2c97ff 100644 --- a/cli/src/commands/upload/interface.rs +++ b/cli/src/commands/upload/interface.rs @@ -3,29 +3,35 @@ use std::path::Path; use crate::{ cli::UploadInfo, commands::{ - connect::config::Config, + connect::{config::Config, config_option::ConfigOption}, upload::chunk_reader::ChunkReader, }, - manifest::generate_manifest, operator_builder::OperatorBuilder, + manifest::{CompressionOption, DepotManifest, generate_v2_manifest}, + operator_builder::OperatorBuilder, }; use futures::AsyncWriteExt; use log::info; +use opendal::Operator; use tokio_util::compat::FuturesAsyncWriteCompatExt; -pub async fn upload(info: &UploadInfo, config: Config) -> anyhow::Result<()> { +pub async fn upload( + info: &UploadInfo, + config: Config, + name: &Option, +) -> anyhow::Result<()> { let game_id = &info.game_id; let path = &info.path; let version_id = &info.version_id; - let manifest = generate_manifest(Path::new(path)).await?; - let operator = match info.upload_style { - crate::cli::UploadStyle::S3 => config - .get_active_s3() - .ok_or(anyhow::Error::msg("Could not get active S3 value"))? - .build()?, - }; + let operator = get_operator(config, name)?; + + let mut existing_depot_manifest = get_depot_manifest(&operator).await?; + + let v2_manifest = generate_v2_manifest(Path::new(path)).await?; + info!("Uploading chunks"); - for (id, data) in &manifest.chunks { + + for (id, data) in &v2_manifest.chunks { info!("Uploading chunk id {id}"); let mut reader = ChunkReader::new(path, data); let mut writer = operator @@ -36,6 +42,35 @@ pub async fn upload(info: &UploadInfo, config: Config) -> anyhow::Result<()> { tokio::io::copy(&mut reader, &mut writer).await?; writer.into_inner().close().await?; } + info!("Finished uploading chunks"); + + existing_depot_manifest.append( + game_id.to_string(), + version_id.to_string(), + CompressionOption::None, + ); Ok(()) } + +async fn get_depot_manifest(operator: &Operator) -> Result { + let existing_depot_manifest = operator.read("manifest.json").await?.to_bytes(); + let existing_depot_manifest: DepotManifest = + serde_json::from_slice(existing_depot_manifest.as_ref())?; + Ok(existing_depot_manifest) +} + +fn get_operator(config: Config, name: &Option) -> anyhow::Result { + let operator = match if let Some(name) = name { + config + .get(name) + .ok_or(anyhow::anyhow!("Name does not exist"))? + } else { + config.get_active().ok_or(anyhow::anyhow!( + "No active connection set. Please specify with --name" + ))? + } { + ConfigOption::S3(s3_config) => s3_config.build()?, + }; + Ok(operator) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 627a1490..0a0be5c4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,12 +18,13 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let mut config = Config::read(); - match &cli.command { + match cli.command { Commands::Connect { name, option } => { manage_configuration(&mut config, name, option).await? } - Commands::Upload(info) => { - upload::interface::upload(info, config).await?; + Commands::Upload { info, name } => { + let info = info.interactive_configure(); + upload::interface::upload(&info, config, &name).await?; } }; diff --git a/cli/src/manifest.rs b/cli/src/manifest.rs index e6732233..fe766f15 100644 --- a/cli/src/manifest.rs +++ b/cli/src/manifest.rs @@ -37,13 +37,13 @@ impl DepotManifest { } } -pub async fn generate_manifest(dir: &Path) -> anyhow::Result { +pub async fn generate_v2_manifest(dir: &Path) -> anyhow::Result { let progress_bar = ProgressBar::new(10_000).with_style( ProgressStyle::default_bar() .template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%") .unwrap(), ); - + generate_manifest_rusty( dir, |progress| {