use crate::poka_yoke::ValidatedPath;
use crate::utils::error::Result;
#[derive(Debug, Clone)]
pub enum Operation {
FileCreate { path: ValidatedPath, size: u64 },
FileWrite { path: ValidatedPath, size: u64 },
FileDelete { path: ValidatedPath },
CommandExec { command: String, args: Vec<String> },
DirCreate { path: ValidatedPath },
DirDelete { path: ValidatedPath },
}
pub struct DryRunMode {
operations: Vec<Operation>,
executed: bool,
}
impl DryRunMode {
pub fn new() -> Self {
Self {
operations: Vec::new(),
executed: false,
}
}
pub fn add_operation(&mut self, operation: Operation) {
self.operations.push(operation);
}
pub fn preview(&self) {
println!("📋 Dry Run Preview:");
println!(" {} operations planned:", self.operations.len());
println!();
for (i, op) in self.operations.iter().enumerate() {
match op {
Operation::FileCreate { path, size } => {
println!(" {}. CREATE {} ({} bytes)", i + 1, path, size);
}
Operation::FileWrite { path, size } => {
println!(" {}. WRITE {} ({} bytes)", i + 1, path, size);
}
Operation::FileDelete { path } => {
println!(" {}. DELETE {}", i + 1, path);
}
Operation::CommandExec { command, args } => {
println!(" {}. EXEC {} {}", i + 1, command, args.join(" "));
}
Operation::DirCreate { path } => {
println!(" {}. MKDIR {}", i + 1, path);
}
Operation::DirDelete { path } => {
println!(" {}. RMDIR {}", i + 1, path);
}
}
}
}
pub fn confirm(&self) -> Result<bool> {
print!("\nProceed with these operations? [y/N]: ");
use std::io::Write;
std::io::stdout().flush().map_err(|e| {
crate::utils::error::Error::io_error(format!("Failed to flush stdout: {}", e))
})?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).map_err(|e| {
crate::utils::error::Error::io_error(format!("Failed to read stdin: {}", e))
})?;
Ok(input.trim().to_lowercase().starts_with('y'))
}
pub fn execute(&mut self) -> Result<()> {
if self.executed {
return Err(crate::utils::error::Error::new(
"Operations already executed",
));
}
for op in &self.operations {
self.validate_operation(op)?;
}
let mut completed = Vec::new();
for op in &self.operations {
match self.execute_operation(op) {
Ok(()) => completed.push(op.clone()),
Err(e) => {
log::error!("Operation failed: {}", e);
self.rollback(&completed)?;
return Err(e);
}
}
}
self.executed = true;
Ok(())
}
fn validate_operation(&self, op: &Operation) -> Result<()> {
match op {
Operation::FileCreate { path, .. } | Operation::FileWrite { path, .. } => {
if let Some(parent) = path.as_path().parent() {
if !parent.exists() {
return Err(crate::utils::error::Error::invalid_input(format!(
"Parent directory does not exist: {}",
parent.display()
)));
}
}
}
Operation::FileDelete { path } | Operation::DirDelete { path } => {
if !path.as_path().exists() {
return Err(crate::utils::error::Error::invalid_input(format!(
"Path does not exist: {}",
path
)));
}
}
Operation::CommandExec { .. } | Operation::DirCreate { .. } => {
}
}
Ok(())
}
fn execute_operation(&self, op: &Operation) -> Result<()> {
match op {
Operation::FileCreate { path, .. } | Operation::FileWrite { path, .. } => {
std::fs::write(path.as_path(), b"").map_err(|e| {
crate::utils::error::Error::io_error(format!("Failed to write file: {}", e))
})?;
}
Operation::FileDelete { path } => {
std::fs::remove_file(path.as_path()).map_err(|e| {
crate::utils::error::Error::io_error(format!("Failed to delete file: {}", e))
})?;
}
Operation::CommandExec { command, args } => {
let status = std::process::Command::new(command)
.args(args)
.status()
.map_err(|e| {
crate::utils::error::Error::new(&format!(
"Failed to execute command: {}",
e
))
})?;
if !status.success() {
return Err(crate::utils::error::Error::new(&format!(
"Command failed with exit code: {:?}",
status.code()
)));
}
}
Operation::DirCreate { path } => {
std::fs::create_dir_all(path.as_path()).map_err(|e| {
crate::utils::error::Error::io_error(format!(
"Failed to create directory: {}",
e
))
})?;
}
Operation::DirDelete { path } => {
std::fs::remove_dir_all(path.as_path()).map_err(|e| {
crate::utils::error::Error::io_error(format!(
"Failed to delete directory: {}",
e
))
})?;
}
}
Ok(())
}
fn rollback(&self, completed: &[Operation]) -> Result<()> {
println!("\n⚠️ Rolling back {} operations...", completed.len());
for op in completed.iter().rev() {
match self.rollback_operation(op) {
Ok(()) => log::info!("Rolled back: {:?}", op),
Err(e) => log::error!("Rollback failed: {}", e),
}
}
Ok(())
}
fn rollback_operation(&self, op: &Operation) -> Result<()> {
match op {
Operation::FileCreate { path, .. } | Operation::FileWrite { path, .. } => {
let _ = std::fs::remove_file(path.as_path());
}
Operation::FileDelete { .. } => {
log::warn!("Cannot rollback file deletion");
}
Operation::CommandExec { .. } => {
log::warn!("Cannot rollback command execution");
}
Operation::DirCreate { path } => {
let _ = std::fs::remove_dir_all(path.as_path());
}
Operation::DirDelete { .. } => {
log::warn!("Cannot rollback directory deletion");
}
}
Ok(())
}
}
impl Default for DryRunMode {
fn default() -> Self {
Self::new()
}
}