Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -72,4 +87,9 @@ COMMANDS
Deletes a multipart upload in a bucket
multipart delete <bucket_name> <-a | --all>
Deletes all multipart upload in a bucket

provider set <provider_name>
Sets the desired S3-compatible provider based on config.toml
provider get
Gets all the S3-compatible providers configured in config.toml
```
13 changes: 13 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub enum Commands {
#[command(subcommand)]
commands: MultpartCommands,
},
Provider {
#[command(subcommand)]
commands: ProviderCommands,
},
Init {},
}

Expand Down Expand Up @@ -107,3 +111,12 @@ pub enum MultpartCommands {
bucket: String,
},
}

#[derive(Subcommand, Debug)]
pub enum ProviderCommands {
Get,
Set {
#[arg(required = true)]
provider_name: String,
},
}
13 changes: 9 additions & 4 deletions src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Keys>,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
/// Keys structure containing S3 credentials and URL endpoint
pub struct Keys {
pub key_id: String,
Expand Down Expand Up @@ -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 = ""
Expand Down
11 changes: 6 additions & 5 deletions src/client/init.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
46 changes: 32 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -21,17 +22,25 @@ mod buckets;
mod cli;
mod client;
mod files;
mod misc;
mod multipart;
mod util;

#[::tokio::main]
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();

Expand All @@ -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?;
Expand Down Expand Up @@ -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?;
Expand All @@ -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?;
Expand All @@ -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?;
Expand All @@ -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?;
Expand All @@ -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?;
Expand All @@ -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?;
Expand Down Expand Up @@ -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?;
Expand All @@ -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?;
Expand All @@ -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?;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/misc/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod provider;
63 changes: 63 additions & 0 deletions src/misc/provider.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<&str>>();

let endpoint_domain = endpoint_split
.iter()
.skip(endpoint_split.len().saturating_sub(2))
.copied()
.collect::<Vec<_>>()
.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::<DocumentMut>()?;
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(())
}
Loading