use anyhow::Result;
use colored::Colorize;
use serde::Serialize;
use crate::output::OutputFormat;
use raps_oss::OssClient;
use super::format_size;
#[derive(Serialize)]
struct CopyObjectOutput {
success: bool,
source_bucket: String,
source_object: String,
dest_bucket: String,
dest_object: String,
size: u64,
size_human: String,
message: String,
}
pub(super) async fn copy_object(
client: &OssClient,
source_bucket: &str,
source_object: &str,
dest_bucket: &str,
dest_object: Option<&str>,
output_format: OutputFormat,
) -> Result<()> {
let destination_key = dest_object.unwrap_or(source_object);
if output_format.supports_colors() {
println!(
"{} {} {} {}",
"Copying".dimmed(),
format!("{}/{}", source_bucket, source_object).cyan(),
"to".dimmed(),
format!("{}/{}", dest_bucket, destination_key).cyan()
);
}
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join(format!(
"raps_copy_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
client
.download_object(source_bucket, source_object, &temp_path)
.await
.inspect_err(|_| {
let _ = std::fs::remove_file(&temp_path);
})?;
let upload_result = client
.upload_object(dest_bucket, destination_key, &temp_path)
.await;
let _ = std::fs::remove_file(&temp_path);
let info = upload_result?;
let output = CopyObjectOutput {
success: true,
source_bucket: source_bucket.to_string(),
source_object: source_object.to_string(),
dest_bucket: dest_bucket.to_string(),
dest_object: destination_key.to_string(),
size: info.size,
size_human: format_size(info.size),
message: format!(
"Copied '{}/{}' to '{}/{}'",
source_bucket, source_object, dest_bucket, destination_key
),
};
match output_format {
OutputFormat::Table => {
println!("{} {}", "✓".green().bold(), output.message);
println!(" {} {}", "Size:".bold(), output.size_human);
}
_ => {
output_format.write(&output)?;
}
}
Ok(())
}
#[derive(Serialize)]
struct RenameObjectOutput {
success: bool,
bucket_key: String,
old_key: String,
new_key: String,
size: u64,
size_human: String,
message: String,
}
pub(super) async fn rename_object(
client: &OssClient,
bucket: &str,
object: &str,
new_key: &str,
output_format: OutputFormat,
) -> Result<()> {
if output_format.supports_colors() {
println!(
"{} {} {} {} {} {}",
"Renaming".dimmed(),
format!("{}/{}", bucket, object).cyan(),
"to".dimmed(),
format!("{}/{}", bucket, new_key).cyan(),
"in".dimmed(),
bucket.cyan()
);
}
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join(format!(
"raps_rename_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
client
.download_object(bucket, object, &temp_path)
.await
.inspect_err(|_| {
let _ = std::fs::remove_file(&temp_path);
})?;
let upload_result = client.upload_object(bucket, new_key, &temp_path).await;
let _ = std::fs::remove_file(&temp_path);
let info = upload_result?;
client.delete_object(bucket, object).await?;
let output = RenameObjectOutput {
success: true,
bucket_key: bucket.to_string(),
old_key: object.to_string(),
new_key: new_key.to_string(),
size: info.size,
size_human: format_size(info.size),
message: format!(
"Renamed '{}/{}' to '{}/{}'",
bucket, object, bucket, new_key
),
};
match output_format {
OutputFormat::Table => {
println!("{} {}", "✓".green().bold(), output.message);
println!(" {} {}", "Old key:".bold(), output.old_key);
println!(" {} {}", "New key:".bold(), output.new_key);
println!(" {} {}", "Size:".bold(), output.size_human);
}
_ => {
output_format.write(&output)?;
}
}
Ok(())
}
pub(super) async fn batch_copy_objects(
client: &OssClient,
source_bucket: &str,
dest_bucket: &str,
prefix: Option<String>,
keys: Option<String>,
output_format: OutputFormat,
) -> Result<()> {
let object_keys = if let Some(keys_str) = keys {
keys_str.split(',').map(|s| s.trim().to_string()).collect()
} else {
let objects = client.list_objects(source_bucket).await?;
let filtered: Vec<String> = objects
.into_iter()
.filter(|o| {
prefix
.as_ref()
.is_none_or(|p| o.object_key.starts_with(p.as_str()))
})
.map(|o| o.object_key)
.collect();
filtered
};
if object_keys.is_empty() {
println!("{}", "No objects to copy.".dimmed());
return Ok(());
}
println!(
"{} {} objects from {} to {}...",
"Copying".dimmed(),
object_keys.len(),
source_bucket.cyan(),
dest_bucket.cyan()
);
let result = client
.batch_copy_objects(source_bucket, dest_bucket, &object_keys)
.await?;
match output_format {
OutputFormat::Table => {
for item in &result.results {
match &item.result {
Ok(_) => println!(" {} {}", "✓".green().bold(), item.key),
Err(e) => println!(" {} {} ({})", "✗".red().bold(), item.key, e),
}
}
println!(
"\n{} copied, {} failed (of {} total)",
result.succeeded.to_string().green(),
result.failed.to_string().red(),
result.total
);
}
_ => {
#[derive(Serialize)]
struct BatchSummary {
total: usize,
succeeded: usize,
failed: usize,
}
output_format.write(&BatchSummary {
total: result.total,
succeeded: result.succeeded,
failed: result.failed,
})?;
}
}
Ok(())
}
pub(super) async fn batch_rename_objects(
client: &OssClient,
bucket: &str,
from_pattern: &str,
to_pattern: &str,
output_format: OutputFormat,
) -> Result<()> {
let objects = client.list_objects(bucket).await?;
let renames: Vec<(String, String)> = objects
.into_iter()
.filter(|o| o.object_key.contains(from_pattern))
.map(|o| {
let new_key = o.object_key.replace(from_pattern, to_pattern);
(o.object_key, new_key)
})
.collect();
if renames.is_empty() {
println!(
"{} No objects match pattern '{}'.",
"⚠".yellow(),
from_pattern
);
return Ok(());
}
println!(
"{} {} objects in {}...",
"Renaming".dimmed(),
renames.len(),
bucket.cyan()
);
let result = client.batch_rename_objects(bucket, &renames).await?;
match output_format {
OutputFormat::Table => {
for item in &result.results {
let new_key = renames
.iter()
.find(|(old, _)| old == &item.key)
.map(|(_, new)| new.as_str())
.unwrap_or("?");
match &item.result {
Ok(_) => println!(" {} {} → {}", "✓".green().bold(), item.key, new_key),
Err(e) => println!(" {} {} ({})", "✗".red().bold(), item.key, e),
}
}
println!(
"\n{} renamed, {} failed (of {} total)",
result.succeeded.to_string().green(),
result.failed.to_string().red(),
result.total
);
}
_ => {
#[derive(Serialize)]
struct BatchSummary {
total: usize,
succeeded: usize,
failed: usize,
}
output_format.write(&BatchSummary {
total: result.total,
succeeded: result.succeeded,
failed: result.failed,
})?;
}
}
Ok(())
}