feat: CLI Configuration and maintainability
This commit is contained in:
+5
-7
@@ -2,6 +2,8 @@ use std::path::PathBuf;
|
||||
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
|
||||
use crate::config::config::ConfigOption;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
@@ -16,16 +18,12 @@ pub struct Cli {
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Configures a new Drop server
|
||||
Configure {
|
||||
/// Endpoint of the Drop server
|
||||
url: String,
|
||||
/// API token for non-interactive configuration.
|
||||
#[arg(short, long)]
|
||||
token: Option<String>,
|
||||
},
|
||||
#[command(subcommand)]
|
||||
Configure(ConfigOption),
|
||||
/// Uploads new game version to depot
|
||||
Upload(UploadInfo),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct UploadInfo {
|
||||
/// Identifies the specific upload style that will be used for the set depot
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
cli::UploadInfo,
|
||||
commands::upload::{s3::S3, uploadable::Uploadable},
|
||||
config::Config,
|
||||
manifest::generate_manifest,
|
||||
cli::UploadInfo, commands::upload::{s3::S3, uploadable::Uploadable}, config::config::Config, manifest::generate_manifest
|
||||
};
|
||||
use log::info;
|
||||
|
||||
@@ -16,7 +13,7 @@ pub async fn upload(info: &UploadInfo, config: Config) -> anyhow::Result<()> {
|
||||
let manifest = generate_manifest(&Path::new(path)).await?;
|
||||
let mut uploader: Box<dyn Uploadable> = match info.upload_style {
|
||||
crate::cli::UploadStyle::S3 => Box::new(S3::new(
|
||||
config
|
||||
&config
|
||||
.get_active_s3()
|
||||
.ok_or(anyhow::Error::msg("Could not get active S3 value"))?,
|
||||
)?),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod interface;
|
||||
pub mod s3;
|
||||
pub mod speedtest;
|
||||
pub mod uploadable;
|
||||
pub mod void;
|
||||
pub mod speedtest;
|
||||
@@ -2,12 +2,11 @@ use crate::{
|
||||
commands::upload::{
|
||||
speedtest::{SPEEDTEST_PATH, Speedtest},
|
||||
uploadable::Uploadable,
|
||||
},
|
||||
config::S3Config,
|
||||
}, config::s3::S3Config,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use droplet_rs::manifest::{ChunkData, Manifest};
|
||||
use s3::{Bucket, creds::Credentials};
|
||||
use s3::Bucket;
|
||||
use serde_json::json;
|
||||
use std::{ops::Deref, path::PathBuf};
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use droplet_rs::manifest::{ChunkData, Manifest};
|
||||
use log::warn;
|
||||
|
||||
use crate::commands::upload::uploadable::Uploadable;
|
||||
|
||||
pub struct VoidUploadable;
|
||||
#[async_trait]
|
||||
impl Uploadable for VoidUploadable {
|
||||
async fn upload_chunk(
|
||||
&mut self,
|
||||
_id: &String,
|
||||
_version: &String,
|
||||
_chunk_id: &String,
|
||||
_chunk: &ChunkData,
|
||||
) -> anyhow::Result<()> {
|
||||
warn!("Uploading chunk to VoidUploader");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_speedtest(&mut self) -> anyhow::Result<()> {
|
||||
warn!("Uploading speedtest to VoidUploader");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_manifest(
|
||||
&mut self,
|
||||
_manifest: Manifest,
|
||||
_game_id: &String,
|
||||
_version_id: &String,
|
||||
) -> anyhow::Result<()> {
|
||||
warn!("Uploading manifest to VoidUploader");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl VoidUploadable {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use s3::{Bucket, Region, creds::Credentials};
|
||||
|
||||
pub struct Config {
|
||||
items: Vec<ConfigItem>,
|
||||
active_s3: Option<String>,
|
||||
}
|
||||
pub struct ConfigItem {
|
||||
name: String,
|
||||
config_option: ConfigOption,
|
||||
}
|
||||
enum ConfigOption {
|
||||
S3(S3Config),
|
||||
}
|
||||
pub struct S3Config {
|
||||
secret_key: String,
|
||||
key_id: String,
|
||||
region: String,
|
||||
bucket_name: String,
|
||||
endpoint: Option<String>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_s3: None,
|
||||
}
|
||||
}
|
||||
pub fn get_active_s3(&self) -> Option<&S3Config> {
|
||||
if let Some(active_s3) = &self.active_s3 {
|
||||
self.items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if item.name == *active_s3 {
|
||||
match &item.config_option {
|
||||
ConfigOption::S3(s3_config) => Some(s3_config),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl S3Config {
|
||||
pub fn generate_bucket(&self) -> anyhow::Result<s3::Bucket> {
|
||||
let credentials =
|
||||
Credentials::new(Some(&self.key_id), Some(&self.secret_key), None, None, None)?;
|
||||
|
||||
let region = if let Some(endpoint) = &self.endpoint {
|
||||
Region::Custom {
|
||||
region: self.region.clone(),
|
||||
endpoint: endpoint.clone(),
|
||||
}
|
||||
} else {
|
||||
Region::from_str(&self.region)?
|
||||
};
|
||||
|
||||
let bucket = Bucket::new(&self.bucket_name, region, credentials)?;
|
||||
|
||||
Ok(*bucket)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use std::{fs, str::FromStr};
|
||||
|
||||
use clap::Subcommand;
|
||||
use dialoguer::{Input, theme::ColorfulTheme};
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::{
|
||||
s3::{S3Config, S3ConfigCli},
|
||||
server::ServerConfig,
|
||||
};
|
||||
|
||||
const CONFIG_DIR: &str = "downpour/config.json";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
items: Vec<ConfigItem>,
|
||||
active_s3: Option<String>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let json = serde_json::to_string(self)?;
|
||||
let save_path = dirs::config_dir()
|
||||
.expect("Apparently your home directory doesn't exist") // Should probably formalise that error
|
||||
.join(CONFIG_DIR);
|
||||
fs::write(save_path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn read() -> Self {
|
||||
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).expect(&format!("Could not read save path {:#?}", &save_path)) {
|
||||
serde_json::from_str(&fs::read_to_string(save_path).unwrap()).unwrap()
|
||||
} else {
|
||||
Config::new()
|
||||
}
|
||||
}
|
||||
pub fn add_item(&mut self, item: ConfigItem) {
|
||||
if matches!(item.config_option, ConfigOption::S3(..)) {
|
||||
self.active_s3 = Some(item.name.clone())
|
||||
}
|
||||
self.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ConfigItem {
|
||||
name: String,
|
||||
config_option: ConfigOption,
|
||||
}
|
||||
#[derive(Subcommand, Serialize, Deserialize)]
|
||||
pub enum ConfigOption {
|
||||
Server(ServerConfig),
|
||||
S3(S3ConfigCli),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_s3: None,
|
||||
}
|
||||
}
|
||||
pub fn get_active_s3(&self) -> Option<S3Config> {
|
||||
if let Some(active_s3) = &self.active_s3 {
|
||||
self.items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if item.name == *active_s3 {
|
||||
match &item.config_option {
|
||||
ConfigOption::S3(s3_config) => Some(s3_config),
|
||||
_ => {
|
||||
warn!("Name {} is not of type 'S3'", item.name);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.cloned()
|
||||
.map(|c| c.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use crate::config::config::Config;
|
||||
|
||||
/// Trait which represents data which may be stored in `config_dir/downpour/config.json`
|
||||
pub trait Configurable {
|
||||
fn configure(&self, config: &mut Config);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod configurable;
|
||||
pub mod s3;
|
||||
pub mod server;
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::Args;
|
||||
use dialoguer::{Input, theme::ColorfulTheme};
|
||||
use s3::{Bucket, Region, creds::Credentials};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{config::configurable::Configurable, interactive_optional_variable, interactive_variable};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Args, Clone)]
|
||||
pub struct S3ConfigCli {
|
||||
secret_key: Option<String>,
|
||||
key_id: Option<String>,
|
||||
region: Option<String>,
|
||||
bucket_name: Option<String>,
|
||||
endpoint: Option<String>,
|
||||
}
|
||||
|
||||
impl From<S3ConfigCli> for S3Config {
|
||||
fn from(value: S3ConfigCli) -> Self {
|
||||
interactive_variable!(value, secret_key, "S3 Secret Key");
|
||||
interactive_variable!(value, key_id, "S3 Key ID");
|
||||
interactive_variable!(value, region, "S3 Region");
|
||||
interactive_variable!(value, bucket_name, "S3 Bucket Name");
|
||||
interactive_optional_variable!(value, endpoint, "S3 Endpoint (leave blank for none");
|
||||
Self {
|
||||
secret_key,
|
||||
key_id,
|
||||
region,
|
||||
bucket_name,
|
||||
endpoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct S3Config {
|
||||
secret_key: String,
|
||||
key_id: String,
|
||||
region: String,
|
||||
bucket_name: String,
|
||||
endpoint: Option<String>,
|
||||
}
|
||||
|
||||
impl S3Config {
|
||||
pub fn generate_bucket(&self) -> anyhow::Result<s3::Bucket> {
|
||||
let credentials =
|
||||
Credentials::new(Some(&self.key_id), Some(&self.secret_key), None, None, None)?;
|
||||
|
||||
let region = if let Some(endpoint) = &self.endpoint {
|
||||
Region::Custom {
|
||||
region: self.region.clone(),
|
||||
endpoint: endpoint.clone(),
|
||||
}
|
||||
} else {
|
||||
Region::from_str(&self.region)?
|
||||
};
|
||||
|
||||
let bucket = Bucket::new(&self.bucket_name, region, credentials)?;
|
||||
|
||||
Ok(*bucket)
|
||||
}
|
||||
}
|
||||
|
||||
impl Configurable for S3Config {
|
||||
fn configure(&self, config: &mut super::config::Config) {
|
||||
println!("Configuring S3Config with {:?}", self);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use clap::Args;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::configurable::Configurable;
|
||||
|
||||
#[derive(Serialize, Deserialize, Args, Clone)]
|
||||
pub struct ServerConfig {
|
||||
/// Endpoint of the Drop server
|
||||
url: String,
|
||||
#[arg(short, long)]
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl Configurable for ServerConfig {
|
||||
fn configure(&self, config: &mut super::config::Config) {
|
||||
println!("Configured ServerConfig")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use dialoguer::{Input, theme::ColorfulTheme};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! interactive_variable {
|
||||
($value:ident, $var:ident, $prompt:expr) => {
|
||||
let $var = if let Some($var) = $value.$var {
|
||||
$var
|
||||
} else {
|
||||
crate::interactive::query_variable($prompt).unwrap()
|
||||
};
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! interactive_optional_variable {
|
||||
($value:ident, $var:ident, $prompt:expr) => {
|
||||
let $var = if let Some($var) = $value.$var {
|
||||
Some($var)
|
||||
} else {
|
||||
crate::interactive::query_optional_variable($prompt).unwrap()
|
||||
};
|
||||
};
|
||||
}
|
||||
pub fn query_variable<T: Clone + FromStr + ToString>(prompt: impl ToString) -> dialoguer::Result<T>
|
||||
where
|
||||
<T as FromStr>::Err: ToString,
|
||||
{
|
||||
Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt.to_string())
|
||||
.interact_text()
|
||||
}
|
||||
pub fn query_optional_variable<T: Clone + FromStr + ToString>(
|
||||
prompt: impl ToString,
|
||||
) -> dialoguer::Result<Option<T>>
|
||||
where
|
||||
<T as FromStr>::Err: ToString,
|
||||
{
|
||||
let input: T = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt.to_string())
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
if input.to_string().len() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(input))
|
||||
}
|
||||
+22
-7
@@ -1,6 +1,11 @@
|
||||
use crate::{
|
||||
cli::{Cli, Commands},
|
||||
commands::{configure::interactive_configure, upload}, config::Config,
|
||||
commands::{configure::interactive_configure, upload},
|
||||
config::{
|
||||
config::{Config, ConfigOption},
|
||||
configurable::Configurable,
|
||||
s3::S3Config,
|
||||
},
|
||||
};
|
||||
use clap::Parser;
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
@@ -13,17 +18,18 @@ mod commands;
|
||||
mod config;
|
||||
mod manifest;
|
||||
|
||||
#[macro_use]
|
||||
pub mod interactive;
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
configure_logging()?;
|
||||
let cli = Cli::parse();
|
||||
let config = Config::new();
|
||||
let mut config = Config::read();
|
||||
match &cli.command {
|
||||
Commands::Configure { url, token } => {
|
||||
if let Some(token) = token {
|
||||
} else {
|
||||
interactive_configure(url.to_string()).await?;
|
||||
}
|
||||
Commands::Configure(options) => {
|
||||
configure_command(&mut config, options).await?;
|
||||
}
|
||||
Commands::Upload(info) => {
|
||||
upload::interface::upload(info, config).await?;
|
||||
@@ -33,6 +39,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn configure_command(config: &mut Config, options: &ConfigOption) -> anyhow::Result<()> {
|
||||
let configuration: Box<dyn Configurable> = match options {
|
||||
ConfigOption::Server(options) => Box::new(options.clone()),
|
||||
ConfigOption::S3(options) => Box::new(S3Config::from(options.clone())),
|
||||
};
|
||||
configuration.configure(config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn configure_logging() -> anyhow::Result<()> {
|
||||
let log_level = env::var("RUST_LOG")
|
||||
.unwrap_or_else(|_| "info".to_string())
|
||||
|
||||
Reference in New Issue
Block a user