diff --git a/Cargo.lock b/Cargo.lock index 4e11fd7..9baa62d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2018,7 +2018,7 @@ dependencies = [ [[package]] name = "pepper-s3-cli" -version = "1.5.0" +version = "1.6.0" dependencies = [ "anyhow", "aws-config", @@ -2032,6 +2032,7 @@ dependencies = [ "tabled", "tokio", "toml", + "toml_edit 0.25.11+spec-1.1.0", "tree_magic", "walkdir", ] @@ -2114,7 +2115,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -3014,6 +3015,19 @@ dependencies = [ "winnow 0.7.13", ] +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.1", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -3568,6 +3582,9 @@ name = "winnow" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr 2.7.5", +] [[package]] name = "wit-bindgen" diff --git a/Cargo.toml b/Cargo.toml index 4cc2b11..ad88fc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pepper-s3-cli" -version = "1.5.0" +version = "1.6.0" edition = "2024" description = "A simple CLI tool to manage S3 buckets and files from the terminal" license = "MIT" @@ -24,5 +24,6 @@ serde = { version = "1.0.219", features = ["derive"] } tabled = "0.20.0" tokio = { version = "1.51.1", features = ["full"] } toml = "1.1.2" +toml_edit = "0.25.11" tree_magic = "0.2.3" walkdir = "2.5.0" diff --git a/README.md b/README.md index f45bc1e..35b6e15 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,21 @@ secret_key = "Secret Key" endpoint_url = "Endpoint URL" ``` +Users who wish to save different S3-compatible configurations can do so by adding a new record +in `config.toml`. For example: + +```toml +[default] +key_id = "Key ID" +secret_key = "Secret Key" +endpoint_url = "Endpoint URL" + +[R2] +key_id = "Key ID" +secret_key = "Secret Key" +endpoint_url = "Endpoint URL" +``` + > [!NOTE] > If AWS S3 is being used, make sure to leave `endpoint_url` blank. `endpoint_url` is only required for non-AWS S3 users. @@ -72,4 +87,9 @@ COMMANDS Deletes a multipart upload in a bucket multipart delete <-a | --all> Deletes all multipart upload in a bucket + + provider set + Sets the desired S3-compatible provider based on config.toml + provider get + Gets all the S3-compatible providers configured in config.toml ``` \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 78efeb6..e1026aa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,6 +21,10 @@ pub enum Commands { #[command(subcommand)] commands: MultpartCommands, }, + Provider { + #[command(subcommand)] + commands: ProviderCommands, + }, Init {}, } @@ -107,3 +111,12 @@ pub enum MultpartCommands { bucket: String, }, } + +#[derive(Subcommand, Debug)] +pub enum ProviderCommands { + Get, + Set { + #[arg(required = true)] + provider_name: String, + }, +} diff --git a/src/client/config.rs b/src/client/config.rs index d206cf8..37db79f 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -2,13 +2,16 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, env, fs, path::PathBuf}; use toml; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] /// Configuration structure for S3 CLI pub struct Config { - pub default: Keys, + pub current_provider: String, + + #[serde(flatten)] + pub providers: HashMap, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] /// Keys structure containing S3 credentials and URL endpoint pub struct Keys { pub key_id: String, @@ -63,7 +66,9 @@ pub fn init_config() -> Result<(), anyhow::Error> { { fs::write( config_dir.join("config.toml"), - r#"[default] + r#"current_provider = "default" + +[default] key_id = "" secret_key = "" endpoint_url = "" diff --git a/src/client/init.rs b/src/client/init.rs index b74a50d..348163e 100644 --- a/src/client/init.rs +++ b/src/client/init.rs @@ -1,18 +1,19 @@ use crate::{ - client::config::{self, save_regions}, - client::s3_client::build_client, + client::{ + config::{self, Keys, save_regions}, + s3_client::build_client, + }, util::get_bucket_region, }; /// Initializes the regions file by fetching the regions of all existing buckets using the default client configuration. /// # Returns /// * `Result<(), anyhow::Error>` - `Ok(())` if successful, error if the operation fails -pub async fn init_regions() -> Result<(), anyhow::Error> { - let config: config::Config = config::get_config()?; +pub async fn init_regions(provider: &Keys) -> Result<(), anyhow::Error> { let mut regions: config::Regions = config::get_regions()?; let default_client: aws_sdk_s3::Client = - build_client(&config.default, "us-east-1".to_string()).await?; + build_client(provider, "us-east-1".to_string()).await?; let buckets = default_client.list_buckets().send().await?; for b in buckets.buckets().iter() { diff --git a/src/main.rs b/src/main.rs index f47a0cc..9ac3c37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,15 @@ use crate::{ buckets::{create::create_bucket, delete::delete_bucket, list_buckets::list_buckets}, - cli::{BucketCommands, Cli, Commands, FileCommands, MultpartCommands}, + cli::{BucketCommands, Cli, Commands, FileCommands, MultpartCommands, ProviderCommands}, client::{ - config::{Config, Regions, get_config, get_regions, init_config}, + config::{Config, Keys, Regions, get_config, get_regions, init_config}, init::init_regions, s3_client::build_client, }, files::{ delete::delete_file, download::download_file, list_files::list_files, upload::upload_file, }, + misc::provider::{get_provider, set_provider}, multipart::delete::{delete_all_multipart_uploads, delete_multipart_upload}, util::get_bucket_region, }; @@ -21,6 +22,7 @@ mod buckets; mod cli; mod client; mod files; +mod misc; mod multipart; mod util; @@ -28,10 +30,17 @@ mod util; async fn main() -> Result<()> { init_config()?; - let config: Config = get_config()?; + let mut config: Config = get_config()?; let mut regions: Regions = get_regions()?; - let default_client: Client = build_client(&config.default, "us-east-1".to_string()).await?; + let cloned_config: Config = config.clone(); + + let provider: &Keys = cloned_config + .providers + .get(&config.current_provider) + .ok_or_else(|| anyhow::anyhow!("Active provider not found in config"))?; + + let default_client: Client = build_client(provider, "us-east-1".to_string()).await?; let cli: Cli = Cli::parse(); @@ -47,7 +56,7 @@ async fn main() -> Result<()> { } BucketCommands::Delete { name } => { let client: Client = build_client( - &config.default, + provider, get_bucket_region(&mut regions, name.clone(), &default_client).await?, ) .await?; @@ -76,7 +85,7 @@ async fn main() -> Result<()> { Commands::Files { commands } => match commands { FileCommands::List { bucket } => { let client: Client = build_client( - &config.default, + provider, get_bucket_region(&mut regions, bucket.clone(), &default_client).await?, ) .await?; @@ -90,7 +99,7 @@ async fn main() -> Result<()> { yes, } => { let client: Client = build_client( - &config.default, + provider, get_bucket_region(&mut regions, bucket.clone(), &default_client).await?, ) .await?; @@ -100,7 +109,7 @@ async fn main() -> Result<()> { &client, bucket, key.clone(), - !config.default.endpoint_url.contains("cloudflare"), + !provider.endpoint_url.contains("cloudflare"), force, ) .await?; @@ -126,7 +135,7 @@ async fn main() -> Result<()> { &client, bucket, key.clone(), - !config.default.endpoint_url.contains("cloudflare"), + !provider.endpoint_url.contains("cloudflare"), force, ) .await?; @@ -145,7 +154,7 @@ async fn main() -> Result<()> { override_filename, } => { let client: Client = build_client( - &config.default, + provider, get_bucket_region(&mut regions, bucket.clone(), &default_client).await?, ) .await?; @@ -161,7 +170,7 @@ async fn main() -> Result<()> { verbose, } => { let client: Client = build_client( - &config.default, + provider, get_bucket_region(&mut regions, bucket.clone(), &default_client).await?, ) .await?; @@ -193,7 +202,7 @@ async fn main() -> Result<()> { timestamp_id, } => { let client: Client = build_client( - &config.default, + provider, get_bucket_region(&mut regions, bucket.clone(), &default_client).await?, ) .await?; @@ -207,7 +216,7 @@ async fn main() -> Result<()> { } MultpartCommands::List { bucket } => { let client: Client = build_client( - &config.default, + provider, get_bucket_region(&mut regions, bucket.clone(), &default_client).await?, ) .await?; @@ -218,8 +227,17 @@ async fn main() -> Result<()> { ); } }, + Commands::Provider { commands } => match commands { + ProviderCommands::Get => { + get_provider(&config)?; + } + ProviderCommands::Set { provider_name } => { + set_provider(&mut config, provider_name)?; + init_regions(provider).await?; + } + }, Commands::Init {} => { - init_regions().await?; + init_regions(provider).await?; } } diff --git a/src/misc/mod.rs b/src/misc/mod.rs new file mode 100644 index 0000000..8336397 --- /dev/null +++ b/src/misc/mod.rs @@ -0,0 +1 @@ +pub mod provider; diff --git a/src/misc/provider.rs b/src/misc/provider.rs new file mode 100644 index 0000000..aa2faed --- /dev/null +++ b/src/misc/provider.rs @@ -0,0 +1,63 @@ +use std::fs; + +use anyhow::bail; +use toml_edit::DocumentMut; + +use crate::client::config::{Config, get_config_dir}; + +/// Gets the current active provider and lists all available providers along with their endpoint domains +/// # Arguments +/// * `config` - A reference to the configuration struct containing provider information +/// # Returns +/// * `Result<(), anyhow::Error>` - `Ok(())` if successful, error if the operation fails +pub fn get_provider(config: &Config) -> Result<(), anyhow::Error> { + println!("Current provider: {:?}", config.current_provider); + + println!("Available providers:"); + for (name, key) in config.providers.iter() { + let endpoint_split = key.endpoint_url.split('.').collect::>(); + + let endpoint_domain = endpoint_split + .iter() + .skip(endpoint_split.len().saturating_sub(2)) + .copied() + .collect::>() + .join("."); + + println!( + "- {} (Endpoint domain: {})", + name, + if endpoint_domain.is_empty() { + "Unconfigured".to_string() + } else { + endpoint_domain + } + ); + } + + Ok(()) +} + +/// Sets the desired provider for S3 operations +/// # Arguments +/// * `config` - A mutable reference to the configuration struct +/// * `provider_name` - The name of the provider to set as active +/// # Returns +/// * `Result<(), anyhow::Error>` - `Ok(())` if successful, error if the operation fails +pub fn set_provider(config: &mut Config, provider_name: String) -> Result<(), anyhow::Error> { + if !config.providers.contains_key(&provider_name) { + bail!("Provider {:?} not found in config", provider_name); + } + + config.current_provider = provider_name.clone(); + + let content = fs::read_to_string(get_config_dir().join("config.toml"))?; + let mut doc = content.parse::()?; + doc["current_provider"] = provider_name.clone().into(); + + fs::write(get_config_dir().join("config.toml"), doc.to_string())?; + + println!("Set active provider to {:?}", provider_name); + + Ok(()) +}