feat: Add name default and manual configuration
This commit is contained in:
+29
-7
@@ -1,6 +1,6 @@
|
|||||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
use crate::commands::connect::config_option::ConfigOptionCli;
|
use crate::{commands::connect::config_option::ConfigOptionCli, interactive_variable};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
@@ -18,27 +18,49 @@ pub enum Commands {
|
|||||||
/// Configures downpour endpoints
|
/// Configures downpour endpoints
|
||||||
Connect {
|
Connect {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
name: String,
|
name: Option<String>,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
option: ConfigOptionCli,
|
option: ConfigOptionCli,
|
||||||
},
|
},
|
||||||
/// Uploads new game version to depot
|
/// 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)]
|
#[derive(Args)]
|
||||||
pub struct UploadInfo {
|
pub struct UploadInfo {
|
||||||
/// Identifies the specific upload style that will be used for the set depot
|
pub path: String,
|
||||||
pub upload_style: UploadStyle,
|
pub game_id: String,
|
||||||
|
pub version_id: String,
|
||||||
|
}
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct UploadInfoCli {
|
||||||
/// Relative path to new version files
|
/// Relative path to new version files
|
||||||
#[arg(short, long, default_value_t = String::from("."))]
|
#[arg(short, long, default_value_t = String::from("."))]
|
||||||
pub path: String,
|
pub path: String,
|
||||||
/// ID of game to attach to
|
/// ID of game to attach to
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub game_id: String,
|
pub game_id: Option<String>,
|
||||||
/// Version ID to attach to
|
/// Version ID to attach to
|
||||||
#[arg(short, long)]
|
#[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)]
|
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::{
|
commands::connect::{
|
||||||
connect::{
|
config_option::{ConfigOption, ConfigOptionCli},
|
||||||
config_option::{ConfigOption, ConfigOptionCli},
|
configurable::Configure,
|
||||||
configurable::Configure,
|
speedtest::{SPEEDTEST_PATH, Speedtest},
|
||||||
s3::S3Config,
|
|
||||||
speedtest::{SPEEDTEST_PATH, Speedtest}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
manifest::DepotManifest,
|
manifest::DepotManifest,
|
||||||
};
|
};
|
||||||
use dialoguer::{Confirm, theme::ColorfulTheme};
|
use dialoguer::{Confirm, theme::ColorfulTheme};
|
||||||
use futures::AsyncWriteExt;
|
use futures::AsyncWriteExt;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info};
|
||||||
use opendal::Operator;
|
use opendal::Operator;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, fs, ops::Not};
|
use std::{collections::HashMap, fs, ops::Not};
|
||||||
@@ -23,13 +20,13 @@ const CONFIG_DIR: &str = "downpour/config.json";
|
|||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
configurations: HashMap<String, ConfigOption>,
|
configurations: HashMap<String, ConfigOption>,
|
||||||
active_s3: Option<String>,
|
active: Option<String>,
|
||||||
}
|
}
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
configurations: HashMap::new(),
|
configurations: HashMap::new(),
|
||||||
active_s3: None,
|
active: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn exists(&self, name: &String) -> bool {
|
pub fn exists(&self, name: &String) -> bool {
|
||||||
@@ -48,7 +45,9 @@ impl Config {
|
|||||||
let save_path = dirs::config_dir()
|
let save_path = dirs::config_dir()
|
||||||
.expect("Apparently your home directory doesn't exist") // Should probably formalise that error
|
.expect("Apparently your home directory doesn't exist") // Should probably formalise that error
|
||||||
.join(CONFIG_DIR);
|
.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()
|
serde_json::from_str(&fs::read_to_string(save_path).unwrap()).unwrap()
|
||||||
} else {
|
} else {
|
||||||
Config::new()
|
Config::new()
|
||||||
@@ -56,50 +55,37 @@ impl Config {
|
|||||||
}
|
}
|
||||||
pub fn add_item(&mut self, name: String, object: ConfigOption) {
|
pub fn add_item(&mut self, name: String, object: ConfigOption) {
|
||||||
if matches!(object, ConfigOption::S3(..)) {
|
if matches!(object, ConfigOption::S3(..)) {
|
||||||
self.active_s3 = Some(name.clone())
|
self.active = Some(name.clone())
|
||||||
}
|
}
|
||||||
self.configurations.insert(name, object);
|
self.configurations.insert(name, object);
|
||||||
self.save().expect("Failed to save config");
|
self.save().expect("Failed to save config");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_active_s3(&self) -> Option<S3Config> {
|
pub fn get_active(&self) -> Option<&ConfigOption> {
|
||||||
if let Some(active_s3) = &self.active_s3 {
|
if let Some(active) = &self.active {
|
||||||
self.configurations
|
self.configurations.get(active)
|
||||||
.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()
|
|
||||||
} else {
|
} else {
|
||||||
None
|
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())
|
self.configurations.get(name.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn manage_configuration(
|
pub async fn manage_configuration(
|
||||||
config: &mut Config,
|
config: &mut Config,
|
||||||
name: &String,
|
name: Option<String>,
|
||||||
option: &ConfigOptionCli,
|
option: ConfigOptionCli,
|
||||||
) -> anyhow::Result<()> {
|
) -> 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())
|
let confirm = Confirm::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt(format!(
|
.with_prompt(format!(
|
||||||
"An entry already exists with the name \"{}\". Would you like to overwrite it?",
|
"An entry already exists with the name \"{}\". Would you like to overwrite it?",
|
||||||
&name
|
name
|
||||||
))
|
))
|
||||||
.interact()?;
|
.interact()?;
|
||||||
if !confirm {
|
if !confirm {
|
||||||
@@ -107,9 +93,10 @@ pub async fn manage_configuration(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let config_option = match option {
|
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()?;
|
let operator = config_option.build()?;
|
||||||
|
|
||||||
generate_manifest(&operator).await?;
|
generate_manifest(&operator).await?;
|
||||||
@@ -138,14 +125,15 @@ async fn generate_speedtest(operator: &Operator) -> anyhow::Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut reader = Speedtest::new(|progress| {
|
let mut reader = Speedtest::new(|progress| {
|
||||||
let progress_int = (progress * 100f32).round() as u64;
|
let progress_int = (progress * 100f32).round() as u64;
|
||||||
progress_bar.set_position(progress_int);
|
progress_bar.set_position(progress_int);
|
||||||
});
|
});
|
||||||
let written = tokio::io::copy(&mut reader, &mut writer).await?;
|
let written = tokio::io::copy(&mut reader, &mut writer).await?;
|
||||||
debug!("Wrote {} bytes to {:?}", written, operator.info());
|
debug!("Wrote {} bytes to {:?}", written, operator.info());
|
||||||
writer.into_inner().close().await?;
|
writer.into_inner().close().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_manifest(operator: &Operator) -> anyhow::Result<()> {
|
async fn generate_manifest(operator: &Operator) -> anyhow::Result<()> {
|
||||||
let lister = operator.list_with("manifest.json").limit(1).await?;
|
let lister = operator.list_with("manifest.json").limit(1).await?;
|
||||||
if lister.is_empty().not() {
|
if lister.is_empty().not() {
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ use clap::Subcommand;
|
|||||||
use opendal::{Operator, layers::LoggingLayer};
|
use opendal::{Operator, layers::LoggingLayer};
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Subcommand, Clone)]
|
||||||
pub enum ConfigOptionCli {
|
pub enum ConfigOptionCli {
|
||||||
|
// Connect to any S3-compatible endpoint
|
||||||
S3(S3ConfigCli),
|
S3(S3ConfigCli),
|
||||||
}
|
}
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
@@ -15,7 +19,6 @@ pub enum ConfigOption {
|
|||||||
|
|
||||||
impl ConfigOption {
|
impl ConfigOption {
|
||||||
pub fn build(&self) -> anyhow::Result<Operator> {
|
pub fn build(&self) -> anyhow::Result<Operator> {
|
||||||
|
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
ConfigOption::S3(s3_config) => s3_config.build()?,
|
ConfigOption::S3(s3_config) => s3_config.build()?,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::commands::connect::config_option::ConfigOption;
|
use crate::commands::connect::config_option::ConfigOption;
|
||||||
|
|
||||||
pub trait Configure {
|
pub trait Configure {
|
||||||
async fn configure(self) -> anyhow::Result<ConfigOption>;
|
async fn configure(self, name: &mut Option<String>) -> anyhow::Result<ConfigOption>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ pub mod s3;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod interactive;
|
pub mod interactive;
|
||||||
pub mod config_option;
|
pub mod config_option;
|
||||||
pub mod speedtest;
|
pub mod speedtest;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::connect::{config_option::ConfigOption, configurable::Configure},
|
commands::connect::{config_option::ConfigOption, configurable::Configure},
|
||||||
interactive_variable, operator_builder::OperatorBuilder,
|
interactive_variable,
|
||||||
|
operator_builder::OperatorBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Args, Clone)]
|
#[derive(Args, Clone)]
|
||||||
@@ -28,12 +29,15 @@ pub struct S3Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Configure for S3ConfigCli {
|
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, key_id, "S3 Key ID");
|
||||||
interactive_variable!(self, secret_key, "S3 Secret Key");
|
interactive_variable!(self, secret_key, "S3 Secret Key");
|
||||||
interactive_variable!(self, region, "S3 Region");
|
interactive_variable!(self, region, "S3 Region");
|
||||||
interactive_variable!(self, bucket_name, "S3 Bucket Name");
|
interactive_variable!(self, bucket_name, "S3 Bucket Name");
|
||||||
interactive_variable!(self, endpoint, "S3 Endpoint");
|
interactive_variable!(self, endpoint, "S3 Endpoint");
|
||||||
|
if let None = name {
|
||||||
|
*name = Some(endpoint.clone());
|
||||||
|
}
|
||||||
Ok(ConfigOption::S3(S3Config {
|
Ok(ConfigOption::S3(S3Config {
|
||||||
secret_key,
|
secret_key,
|
||||||
key_id,
|
key_id,
|
||||||
|
|||||||
@@ -3,29 +3,35 @@ use std::path::Path;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::UploadInfo,
|
cli::UploadInfo,
|
||||||
commands::{
|
commands::{
|
||||||
connect::config::Config,
|
connect::{config::Config, config_option::ConfigOption},
|
||||||
upload::chunk_reader::ChunkReader,
|
upload::chunk_reader::ChunkReader,
|
||||||
},
|
},
|
||||||
manifest::generate_manifest, operator_builder::OperatorBuilder,
|
manifest::{CompressionOption, DepotManifest, generate_v2_manifest},
|
||||||
|
operator_builder::OperatorBuilder,
|
||||||
};
|
};
|
||||||
use futures::AsyncWriteExt;
|
use futures::AsyncWriteExt;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use opendal::Operator;
|
||||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
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 game_id = &info.game_id;
|
||||||
let path = &info.path;
|
let path = &info.path;
|
||||||
let version_id = &info.version_id;
|
let version_id = &info.version_id;
|
||||||
|
|
||||||
let manifest = generate_manifest(Path::new(path)).await?;
|
let operator = get_operator(config, name)?;
|
||||||
let operator = match info.upload_style {
|
|
||||||
crate::cli::UploadStyle::S3 => config
|
let mut existing_depot_manifest = get_depot_manifest(&operator).await?;
|
||||||
.get_active_s3()
|
|
||||||
.ok_or(anyhow::Error::msg("Could not get active S3 value"))?
|
let v2_manifest = generate_v2_manifest(Path::new(path)).await?;
|
||||||
.build()?,
|
|
||||||
};
|
|
||||||
info!("Uploading chunks");
|
info!("Uploading chunks");
|
||||||
for (id, data) in &manifest.chunks {
|
|
||||||
|
for (id, data) in &v2_manifest.chunks {
|
||||||
info!("Uploading chunk id {id}");
|
info!("Uploading chunk id {id}");
|
||||||
let mut reader = ChunkReader::new(path, data);
|
let mut reader = ChunkReader::new(path, data);
|
||||||
let mut writer = operator
|
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?;
|
tokio::io::copy(&mut reader, &mut writer).await?;
|
||||||
writer.into_inner().close().await?;
|
writer.into_inner().close().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Finished uploading chunks");
|
info!("Finished uploading chunks");
|
||||||
|
|
||||||
|
existing_depot_manifest.append(
|
||||||
|
game_id.to_string(),
|
||||||
|
version_id.to_string(),
|
||||||
|
CompressionOption::None,
|
||||||
|
);
|
||||||
Ok(())
|
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
@@ -18,12 +18,13 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let mut config = Config::read();
|
let mut config = Config::read();
|
||||||
match &cli.command {
|
match cli.command {
|
||||||
Commands::Connect { name, option } => {
|
Commands::Connect { name, option } => {
|
||||||
manage_configuration(&mut config, name, option).await?
|
manage_configuration(&mut config, name, option).await?
|
||||||
}
|
}
|
||||||
Commands::Upload(info) => {
|
Commands::Upload { info, name } => {
|
||||||
upload::interface::upload(info, config).await?;
|
let info = info.interactive_configure();
|
||||||
|
upload::interface::upload(&info, config, &name).await?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -37,13 +37,13 @@ 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(
|
let progress_bar = ProgressBar::new(10_000).with_style(
|
||||||
ProgressStyle::default_bar()
|
ProgressStyle::default_bar()
|
||||||
.template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%")
|
.template("[{elapsed_precise}] [ETA {eta}] {bar} {percent_precise}%")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
generate_manifest_rusty(
|
generate_manifest_rusty(
|
||||||
dir,
|
dir,
|
||||||
|progress| {
|
|progress| {
|
||||||
|
|||||||
Reference in New Issue
Block a user