use crate::error::{ExecutionError, ExecutionResult};
use crate::models::RollbackAction;
use ricecoder_storage::PathResolver;
use std::path::Path;
use std::process::Command;
use tracing::{debug, info, warn};
pub struct RestoreFileHandler;
impl RestoreFileHandler {
pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
debug!("Restoring file from backup");
let file_path = action
.data
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ExecutionError::RollbackFailed("Missing file_path in restore action".to_string())
})?;
let backup_path = action
.data
.get("backup_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ExecutionError::RollbackFailed("Missing backup_path in restore action".to_string())
})?;
let resolved_file_path = PathResolver::expand_home(Path::new(file_path))
.map_err(|e| ExecutionError::RollbackFailed(format!("Invalid file path: {}", e)))?;
let resolved_backup_path = PathResolver::expand_home(Path::new(backup_path))
.map_err(|e| ExecutionError::RollbackFailed(format!("Invalid backup path: {}", e)))?;
if !resolved_backup_path.exists() {
return Err(ExecutionError::RollbackFailed(format!(
"Backup file not found: {}",
backup_path
)));
}
if let Some(parent) = resolved_file_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
ExecutionError::RollbackFailed(format!(
"Failed to create parent directories for {}: {}",
file_path, e
))
})?;
}
std::fs::copy(&resolved_backup_path, &resolved_file_path).map_err(|e| {
ExecutionError::RollbackFailed(format!(
"Failed to restore file {} from backup: {}",
file_path, e
))
})?;
let message = format!("Restored {} from backup", file_path);
info!("{}", message);
Ok(message)
}
}
pub struct DeleteFileHandler;
impl DeleteFileHandler {
pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
debug!("Deleting created file");
let file_path = action
.data
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ExecutionError::RollbackFailed("Missing file_path in delete action".to_string())
})?;
let resolved_path = PathResolver::expand_home(Path::new(file_path))
.map_err(|e| ExecutionError::RollbackFailed(format!("Invalid path: {}", e)))?;
if !resolved_path.exists() {
warn!(
file_path = %file_path,
"File to delete does not exist, skipping"
);
return Ok(format!("File {} already deleted", file_path));
}
std::fs::remove_file(&resolved_path).map_err(|e| {
ExecutionError::RollbackFailed(format!("Failed to delete file {}: {}", file_path, e))
})?;
let message = format!("Deleted {}", file_path);
info!("{}", message);
Ok(message)
}
}
pub struct UndoCommandHandler;
impl UndoCommandHandler {
pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
debug!("Running undo command");
let command = action
.data
.get("command")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ExecutionError::RollbackFailed("Missing command in undo action".to_string())
})?;
let args: Vec<String> = action
.data
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let mut cmd = Command::new(command);
cmd.args(&args);
let output = cmd.output().map_err(|e| {
ExecutionError::RollbackFailed(format!(
"Failed to execute undo command {}: {}",
command, e
))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ExecutionError::RollbackFailed(format!(
"Undo command {} failed with exit code {:?}: {}",
command,
output.status.code(),
stderr
)));
}
let message = format!("Executed undo command: {}", command);
info!("{}", message);
Ok(message)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_restore_file_handler_missing_file_path() {
let action = RollbackAction {
action_type: crate::models::RollbackType::RestoreFile,
data: json!({ "backup_path": "/tmp/backup.txt" }),
};
let result = RestoreFileHandler::handle(&action);
assert!(result.is_err());
}
#[test]
fn test_restore_file_handler_missing_backup_path() {
let action = RollbackAction {
action_type: crate::models::RollbackType::RestoreFile,
data: json!({ "file_path": "/tmp/test.txt" }),
};
let result = RestoreFileHandler::handle(&action);
assert!(result.is_err());
}
#[test]
fn test_delete_file_handler_missing_file_path() {
let action = RollbackAction {
action_type: crate::models::RollbackType::DeleteFile,
data: json!({}),
};
let result = DeleteFileHandler::handle(&action);
assert!(result.is_err());
}
#[test]
fn test_delete_file_handler_nonexistent_file() {
let action = RollbackAction {
action_type: crate::models::RollbackType::DeleteFile,
data: json!({ "file_path": "/tmp/nonexistent_file_12345.txt" }),
};
let result = DeleteFileHandler::handle(&action);
assert!(result.is_ok());
let message = result.unwrap();
assert!(message.contains("already deleted"));
}
#[test]
fn test_undo_command_handler_missing_command() {
let action = RollbackAction {
action_type: crate::models::RollbackType::RunCommand,
data: json!({ "args": [] }),
};
let result = UndoCommandHandler::handle(&action);
assert!(result.is_err());
}
#[test]
fn test_undo_command_handler_with_args() {
let action = RollbackAction {
action_type: crate::models::RollbackType::RunCommand,
data: json!({
"command": "echo",
"args": ["test"]
}),
};
let result = UndoCommandHandler::handle(&action);
assert!(result.is_ok());
}
}