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
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 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.3.0"
version = "1.4.0"
edition = "2024"
description = "A simple CLI tool to manage S3 buckets and files from the terminal"
license = "MIT"
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ COMMANDS
Downloads a file from a bucket to a given location (optionally rename it)
files upload <bucket_name> <file_location> [-o <override_uploaded_filename>]
Uploads a file into a bucket (optionally rename it)

multipart list <bucket_name>
Lists all multipart uploads in a bucket
multipart delete <bucket_name> <file_key> <timestamp_id>
Deletes a multipart upload in a bucket
```
3 changes: 1 addition & 2 deletions example.config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[default]
key_id = "Key ID"
secret_key = "Secret Key"
endpoint_url = "Endpoint URL"
is_aws = false
endpoint_url = "Endpoint URL"
22 changes: 22 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ pub enum Commands {
#[command(subcommand)]
commands: FileCommands,
},
Multipart {
#[command(subcommand)]
commands: MultpartCommands,
},
Init {},
}

Expand Down Expand Up @@ -76,3 +80,21 @@ pub enum FileCommands {
override_filename: Option<String>,
},
}

#[derive(Subcommand, Debug)]
pub enum MultpartCommands {
Delete {
#[arg(required = true)]
bucket: String,

#[arg(required = true)]
key: String,

#[arg(required = true)]
timestamp_id: String,
},
List {
#[arg(required = true)]
bucket: String,
},
}
31 changes: 30 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
buckets::{create::create_bucket, delete::delete_bucket, list_buckets::list_buckets},
cli::{BucketCommands, Cli, Commands, FileCommands},
cli::{BucketCommands, Cli, Commands, FileCommands, MultpartCommands},
client::{
config::{Config, Regions, get_config, get_regions, init_config},
init::init_regions,
Expand All @@ -9,6 +9,7 @@ use crate::{
files::{
delete::delete_file, download::download_file, list_files::list_files, upload::upload_file,
},
multipart::delete::delete_multipart_upload,
util::get_bucket_region,
};
use anyhow::{Result, bail};
Expand All @@ -20,6 +21,7 @@ mod buckets;
mod cli;
mod client;
mod files;
mod multipart;
mod util;

#[::tokio::main]
Expand Down Expand Up @@ -161,6 +163,33 @@ async fn main() -> Result<()> {
);
}
},
Commands::Multipart { commands } => match commands {
MultpartCommands::Delete {
bucket,
key,
timestamp_id,
} => {
let client: Client = build_client(
&config.default,
get_bucket_region(&mut regions, bucket.clone(), &default_client).await?,
)
.await?;

delete_multipart_upload(&client, bucket, key, timestamp_id).await?;
}
MultpartCommands::List { bucket } => {
let client: Client = build_client(
&config.default,
get_bucket_region(&mut regions, bucket.clone(), &default_client).await?,
)
.await?;

println!(
"{}",
multipart::list::list_multipart_uploads(&client, &bucket).await?
);
}
},
Commands::Init {} => {
init_regions().await?;
}
Expand Down
63 changes: 63 additions & 0 deletions src/multipart/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use aws_sdk_s3::{Client, operation::list_multipart_uploads::ListMultipartUploadsOutput};
use chrono::{DateTime, Local};

/// Deletes an incomplete multipart upload from an S3 bucket
/// # Arguments
/// * `client` - A reference to the S3 client
/// * `bucket` - The name of the bucket
/// * `key` - The key (path) of the multipart upload to delete
/// * `timestamp_id` - The timestamp ID of the multipart upload to delete
/// # Returns
/// * `Result<(), anyhow::Error>` - `Ok(())` if successful, error if the operation fails
pub async fn delete_multipart_upload(
client: &Client,
bucket: String,
key: String,
timestamp_id: String,
) -> Result<(), anyhow::Error> {
let res: ListMultipartUploadsOutput = client
.list_multipart_uploads()
.bucket(&bucket)
.send()
.await?;

if res.uploads.is_none() {
return Ok(());
}

let upload_id =
res.uploads
.unwrap()
.iter()
.find_map(|u: &aws_sdk_s3::types::MultipartUpload| {
let timestamp =
DateTime::from_timestamp_millis(u.initiated().unwrap().to_millis().unwrap())
.unwrap()
.with_timezone(&Local)
.timestamp_millis();

if timestamp == timestamp_id.parse::<i64>().unwrap() && u.key().unwrap() == key {
Some(u.upload_id.clone())
} else {
None
}
});

if upload_id.is_none() {
return Ok(());
}

if upload_id.as_ref().unwrap().is_none() {
return Ok(());
}

client
.abort_multipart_upload()
.bucket(bucket)
.key(key)
.upload_id(upload_id.unwrap().unwrap())
.send()
.await?;

Ok(())
}
51 changes: 51 additions & 0 deletions src/multipart/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use anyhow::bail;
use aws_sdk_s3::{Client, operation::list_multipart_uploads::ListMultipartUploadsOutput};
use chrono::{DateTime, Local};
use tabled::{Table, Tabled};

use crate::util::build_table;

#[derive(Tabled)]
struct MultipartUploadInfo {
num: usize,
key: String,
initiated: String,
timestamp_id: String,
}

/// Lists incomplete multipart uploads in an S3 bucket
/// # Arguments
/// * `client` - A reference to the S3 client
/// * `bucket` - The name of the bucket
/// # Returns
/// * `Result<Table, anyhow::Error>` - `Table` if successful, error if the operation fails
pub async fn list_multipart_uploads(client: &Client, bucket: &str) -> Result<Table, anyhow::Error> {
let res: ListMultipartUploadsOutput = client
.list_multipart_uploads()
.bucket(bucket)
.send()
.await?;

if res.uploads.is_none() {
bail!("No multipart uploads found in the bucket '{}'", bucket)
}

let table: Table = build_table(
res.uploads.unwrap(),
|i: usize, o: &aws_sdk_s3::types::MultipartUpload| {
let timestamp =
DateTime::from_timestamp_millis(o.initiated().unwrap().to_millis().unwrap())
.unwrap()
.with_timezone(&Local);

MultipartUploadInfo {
num: i + 1,
key: o.key().unwrap().to_string(),
initiated: timestamp.format("%b %d, %Y - %H:%M:%S").to_string(),
timestamp_id: timestamp.timestamp_millis().to_string(),
}
},
);

Ok(table)
}
2 changes: 2 additions & 0 deletions src/multipart/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod delete;
pub mod list;
Loading