feat: Add name default and manual configuration

This commit is contained in:
quexeky
2026-01-26 09:06:48 +11:00
parent 820c1b06f9
commit a72cac7259
9 changed files with 123 additions and 70 deletions
+29 -7
View File
@@ -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<String>,
#[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<String>,
},
}
#[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<String>,
/// Version ID to attach to
#[arg(short, long)]
pub version_id: String,
pub version_id: Option<String>,
}
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)]
+24 -36
View File
@@ -1,18 +1,15 @@
use crate::{
commands::{
connect::{
commands::connect::{
config_option::{ConfigOption, ConfigOptionCli},
configurable::Configure,
s3::S3Config,
speedtest::{SPEEDTEST_PATH, Speedtest}
},
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<String, ConfigOption>,
active_s3: Option<String>,
active: Option<String>,
}
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<S3Config> {
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<T: AsRef<String>>(&self, name: T) -> Option<&ConfigOption> {
pub fn get<T: AsRef<str>>(&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<String>,
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?;
@@ -146,6 +133,7 @@ async fn generate_speedtest(operator: &Operator) -> anyhow::Result<()> {
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() {
+5 -2
View File
@@ -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<Operator> {
Ok(match self {
ConfigOption::S3(s3_config) => s3_config.build()?,
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::commands::connect::config_option::ConfigOption;
pub trait Configure {
async fn configure(self) -> anyhow::Result<ConfigOption>;
async fn configure(self, name: &mut Option<String>) -> anyhow::Result<ConfigOption>;
}
+6 -2
View File
@@ -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<ConfigOption> {
async fn configure(self, name: &mut Option<String>) -> anyhow::Result<ConfigOption> {
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,
+46 -11
View File
@@ -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<String>,
) -> 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<DepotManifest, anyhow::Error> {
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<String>) -> anyhow::Result<Operator> {
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)
}
+4 -3
View File
@@ -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?;
}
};
+1 -1
View File
@@ -37,7 +37,7 @@ impl DepotManifest {
}
}
pub async fn generate_manifest(dir: &Path) -> anyhow::Result<Manifest> {
pub async fn generate_v2_manifest(dir: &Path) -> anyhow::Result<Manifest> {
let progress_bar = ProgressBar::new(10_000).with_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%")