use crate::config::Config;
use crate::types::StoragePath;
use crate::types::error::S3rmError;
use anyhow::{Result, anyhow};
use std::io::{BufRead, IsTerminal, Write};
const ANSI_BOLD_RED: &str = "\x1b[1;31m";
const ANSI_BOLD_YELLOW: &str = "\x1b[1;33m";
const ANSI_RESET: &str = "\x1b[0m";
pub trait PromptHandler: Send + Sync {
fn read_confirmation(&self, target_display: &str, use_color: bool) -> Result<String>;
fn is_interactive(&self) -> bool;
}
pub struct StdioPromptHandler;
impl PromptHandler for StdioPromptHandler {
fn read_confirmation(&self, target_display: &str, use_color: bool) -> Result<String> {
if use_color {
println!(
"\n{}WARNING:{} All objects matching prefix {}{}{} will be deleted.",
ANSI_BOLD_RED, ANSI_RESET, ANSI_BOLD_YELLOW, target_display, ANSI_RESET,
);
} else {
println!(
"\nWARNING: All objects matching prefix {} will be deleted.",
target_display,
);
}
println!(
"Use --dry-run to preview which objects would be deleted without actually removing them.\n"
);
print!("Type 'yes' to confirm deletion: ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().lock().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn is_interactive(&self) -> bool {
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
}
}
pub struct SafetyChecker {
dry_run: bool,
force: bool,
json_logging: bool,
disable_color: bool,
target_display: String,
prompt_handler: Box<dyn PromptHandler>,
}
impl SafetyChecker {
pub fn new(config: &Config) -> Self {
let json_logging = config
.tracing_config
.map(|tc| tc.json_tracing)
.unwrap_or(false);
let disable_color = config
.tracing_config
.map(|tc| tc.disable_color_tracing)
.unwrap_or(false);
Self {
dry_run: config.dry_run,
force: config.force,
json_logging,
disable_color,
target_display: format_target(&config.target),
prompt_handler: Box::new(StdioPromptHandler),
}
}
#[cfg(test)]
pub fn with_prompt_handler(config: &Config, prompt_handler: Box<dyn PromptHandler>) -> Self {
let json_logging = config
.tracing_config
.map(|tc| tc.json_tracing)
.unwrap_or(false);
let disable_color = config
.tracing_config
.map(|tc| tc.disable_color_tracing)
.unwrap_or(false);
Self {
dry_run: config.dry_run,
force: config.force,
json_logging,
disable_color,
target_display: format_target(&config.target),
prompt_handler,
}
}
pub fn check_before_deletion(&self) -> Result<()> {
if self.dry_run {
return Ok(());
}
if self.force {
return Ok(());
}
if self.is_non_interactive() {
return Err(anyhow!(S3rmError::InvalidConfig(
"Cannot run destructive operation without --force (-f) in a non-interactive environment (no TTY). \
Use --force to skip confirmation, or --dry-run to preview deletions."
.to_string()
)));
}
self.prompt_confirmation()
}
fn is_non_interactive(&self) -> bool {
if self.json_logging {
return true;
}
if !self.prompt_handler.is_interactive() {
return true;
}
false
}
fn prompt_confirmation(&self) -> Result<()> {
let use_color = !self.disable_color;
let input = self
.prompt_handler
.read_confirmation(&self.target_display, use_color)?;
if input != "yes" {
return Err(anyhow!(S3rmError::Cancelled));
}
Ok(())
}
}
fn format_target(target: &StoragePath) -> String {
match target {
StoragePath::S3 { bucket, prefix } => {
if prefix.is_empty() {
format!("s3://{}/", bucket)
} else {
format!("s3://{}/{}", bucket, prefix)
}
}
}
}