refactor: Remove ConfigItem
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
use crate::commands::config::{config_option::ConfigOption, s3::S3Config};
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fs};
|
||||
|
||||
const CONFIG_DIR: &str = "downpour/config.json";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
items: HashMap<String, ConfigOption>,
|
||||
active_s3: Option<String>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: HashMap::new(),
|
||||
active_s3: None,
|
||||
}
|
||||
}
|
||||
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::create_dir_all(save_path.parent().unwrap())?;
|
||||
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, name: String, object: ConfigOption) {
|
||||
if matches!(object, ConfigOption::S3(..)) {
|
||||
self.active_s3 = Some(name.clone())
|
||||
}
|
||||
self.items.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.items
|
||||
.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()
|
||||
.map(|c| c.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use clap::Subcommand;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::commands::config::{
|
||||
s3::{S3Config, S3ConfigCli},
|
||||
server::{ServerConfig, ServerConfigCli},
|
||||
};
|
||||
|
||||
#[derive(Subcommand, Clone)]
|
||||
pub enum ConfigOptionCli {
|
||||
Server(ServerConfigCli),
|
||||
S3(S3ConfigCli),
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub enum ConfigOption {
|
||||
Server(ServerConfig),
|
||||
S3(S3Config),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use crate::commands::config::config_option::ConfigOption;
|
||||
|
||||
pub trait Configurable {
|
||||
async fn configure(self) -> anyhow::Result<ConfigOption>;
|
||||
}
|
||||
@@ -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::commands::config::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::commands::config::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))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod config;
|
||||
pub mod configure;
|
||||
pub mod s3;
|
||||
pub mod server;
|
||||
#[macro_use]
|
||||
pub mod interactive;
|
||||
pub mod config_option;
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::Args;
|
||||
use s3::{Bucket, Region, creds::Credentials};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
commands::config::{config_option::ConfigOption, configure::Configurable},
|
||||
interactive_optional_variable, interactive_variable,
|
||||
};
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
pub struct S3ConfigCli {
|
||||
secret_key: Option<String>,
|
||||
key_id: Option<String>,
|
||||
region: Option<String>,
|
||||
bucket_name: Option<String>,
|
||||
endpoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct S3Config {
|
||||
secret_key: String,
|
||||
key_id: String,
|
||||
region: String,
|
||||
bucket_name: String,
|
||||
endpoint: Option<String>,
|
||||
}
|
||||
|
||||
impl Configurable for S3ConfigCli {
|
||||
async fn configure(self) -> anyhow::Result<ConfigOption> {
|
||||
interactive_variable!(self, secret_key, "S3 Secret Key");
|
||||
interactive_variable!(self, key_id, "S3 Key ID");
|
||||
interactive_variable!(self, region, "S3 Region");
|
||||
interactive_variable!(self, bucket_name, "S3 Bucket Name");
|
||||
interactive_optional_variable!(self, endpoint, "S3 Endpoint (leave blank for none");
|
||||
Ok(ConfigOption::S3(S3Config {
|
||||
secret_key,
|
||||
key_id,
|
||||
region,
|
||||
bucket_name,
|
||||
endpoint,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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,92 @@
|
||||
use clap::Args;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use dialoguer::{Confirm, Input, theme::ColorfulTheme};
|
||||
use reqwest::Client;
|
||||
use url::Url;
|
||||
|
||||
use crate::commands::config::{config_option::ConfigOption, configure::Configurable};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct ServerConfig {
|
||||
url: String,
|
||||
token: String,
|
||||
}
|
||||
#[derive(Args, Clone)]
|
||||
pub struct ServerConfigCli {
|
||||
/// Endpoint of the Drop server
|
||||
url: String,
|
||||
#[arg(short, long)]
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
const TOKEN_CREATE_PAYLOAD: &str =
|
||||
"eyJuYW1lIjoiZG93bnBvdXIgKGNsaSkiLCJhY2xzIjpbImRlcG90Om5ldyJdfQ==";
|
||||
|
||||
impl Configurable for ServerConfigCli {
|
||||
async fn configure(self) -> anyhow::Result<ConfigOption> {
|
||||
let base_url = Url::parse(&self.url)?;
|
||||
let mut token_create_url = base_url.join("/admin/settings/tokens")?;
|
||||
{
|
||||
let mut query = token_create_url.query_pairs_mut();
|
||||
query.append_pair("payload", TOKEN_CREATE_PAYLOAD);
|
||||
};
|
||||
|
||||
let confirm = Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(format!(
|
||||
"Open \"{}\" in your default browser?",
|
||||
token_create_url.as_str()
|
||||
))
|
||||
.interact()?;
|
||||
|
||||
if !confirm {
|
||||
return Err(anyhow!("User cancelled action"));
|
||||
}
|
||||
|
||||
webbrowser::open(token_create_url.as_str())?;
|
||||
|
||||
let token: String = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("API token")
|
||||
.interact_text()?;
|
||||
|
||||
validate_configuration(&self.url, &token).await?;
|
||||
|
||||
Ok(ConfigOption::Server(ServerConfig {
|
||||
url: self.url,
|
||||
token,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
static CLIENT: LazyLock<Client> = LazyLock::new(|| reqwest::Client::new());
|
||||
const REQUIRED_ACLS: [&str; 1] = ["depot:new"];
|
||||
|
||||
pub async fn validate_configuration(url: &str, token: &str) -> Result<()> {
|
||||
let base_url = Url::parse(&url)?;
|
||||
let token_check_url = base_url.join("/api/v1/token")?;
|
||||
|
||||
let acl_check = CLIENT
|
||||
.get(token_check_url)
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !acl_check.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
"ACL check failed with response code: {}",
|
||||
acl_check.status()
|
||||
));
|
||||
}
|
||||
|
||||
let acls: Vec<String> = acl_check.json().await?;
|
||||
|
||||
for acl in REQUIRED_ACLS {
|
||||
if !acls.contains(&acl.to_string()) {
|
||||
return Err(anyhow!("Token missing {} acl", acl));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use dialoguer::{Confirm, Input, theme::ColorfulTheme};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
const TOKEN_CREATE_PAYLOAD: &str =
|
||||
"eyJuYW1lIjoiZG93bnBvdXIgKGNsaSkiLCJhY2xzIjpbImRlcG90Om5ldyJdfQ==";
|
||||
|
||||
static CLIENT: LazyLock<Client> = LazyLock::new(|| reqwest::Client::new());
|
||||
const REQUIRED_ACLS: [&str; 1] = ["depot:new"];
|
||||
|
||||
pub async fn interactive_configure(url: String) -> Result<()> {
|
||||
let base_url = Url::parse(&url)?;
|
||||
let mut token_create_url = base_url.join("/admin/settings/tokens")?;
|
||||
{
|
||||
let mut query = token_create_url.query_pairs_mut();
|
||||
query.append_pair("payload", TOKEN_CREATE_PAYLOAD);
|
||||
};
|
||||
|
||||
let confirm = Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(format!(
|
||||
"Open \"{}\" in your default browser?",
|
||||
token_create_url.as_str()
|
||||
))
|
||||
.interact()?;
|
||||
|
||||
if !confirm {
|
||||
return Err(anyhow!("User cancelled action"));
|
||||
}
|
||||
|
||||
webbrowser::open(token_create_url.as_str())?;
|
||||
|
||||
let token: String = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("API token")
|
||||
.interact_text()?;
|
||||
|
||||
validate_configuration(url, token).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn validate_configuration(url: String, token: String) -> Result<()> {
|
||||
let base_url = Url::parse(&url)?;
|
||||
let token_check_url = base_url.join("/api/v1/token")?;
|
||||
|
||||
let acl_check = CLIENT
|
||||
.get(token_check_url)
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !acl_check.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
"ACL check failed with response code: {}",
|
||||
acl_check.status()
|
||||
));
|
||||
}
|
||||
|
||||
let acls: Vec<String> = acl_check.json().await?;
|
||||
|
||||
for acl in REQUIRED_ACLS {
|
||||
if !acls.contains(&acl.to_string()) {
|
||||
return Err(anyhow!("Token missing {} acl", acl));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ServerConfiguration {
|
||||
pub endpoint: String,
|
||||
pub token: String,
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod configure;
|
||||
pub mod config;
|
||||
pub mod upload;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
cli::UploadInfo, commands::upload::{s3::S3, uploadable::Uploadable}, config::config::Config, manifest::generate_manifest
|
||||
cli::UploadInfo,
|
||||
commands::config::config::Config,
|
||||
commands::upload::{s3::S3, uploadable::Uploadable},
|
||||
manifest::generate_manifest,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
commands::config::s3::S3Config,
|
||||
commands::upload::{
|
||||
speedtest::{SPEEDTEST_PATH, Speedtest},
|
||||
uploadable::Uploadable,
|
||||
}, config::s3::S3Config,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use droplet_rs::manifest::{ChunkData, Manifest};
|
||||
|
||||
@@ -30,4 +30,4 @@ impl Speedtest {
|
||||
to_write: SPEEDTEST_BYTES,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,10 @@ pub trait Uploadable {
|
||||
chunk: &ChunkData,
|
||||
) -> anyhow::Result<()>;
|
||||
async fn upload_speedtest(&mut self) -> anyhow::Result<()>;
|
||||
async fn upload_manifest(&mut self, manifest: Manifest, game_id: &String, version_id: &String) -> anyhow::Result<()>;
|
||||
async fn upload_manifest(
|
||||
&mut self,
|
||||
manifest: Manifest,
|
||||
game_id: &String,
|
||||
version_id: &String,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user