ricecoder_execution/
rollback_actions.rs

1//! Rollback action handlers for different action types
2//!
3//! Provides specialized handlers for:
4//! - RestoreFile: Restore files from backup
5//! - DeleteFile: Delete created files
6//! - RunCommand: Execute undo commands
7
8use crate::error::{ExecutionError, ExecutionResult};
9use crate::models::RollbackAction;
10use ricecoder_storage::PathResolver;
11use std::path::Path;
12use std::process::Command;
13use tracing::{debug, info, warn};
14
15/// Handles file restoration from backup
16pub struct RestoreFileHandler;
17
18impl RestoreFileHandler {
19    /// Restore a file from backup
20    ///
21    /// # Arguments
22    /// * `action` - Rollback action containing file and backup paths
23    ///
24    /// # Returns
25    /// Success message if restoration succeeds
26    pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
27        debug!("Restoring file from backup");
28
29        // Extract file path and backup path from action data
30        let file_path = action
31            .data
32            .get("file_path")
33            .and_then(|v| v.as_str())
34            .ok_or_else(|| {
35                ExecutionError::RollbackFailed("Missing file_path in restore action".to_string())
36            })?;
37
38        let backup_path = action
39            .data
40            .get("backup_path")
41            .and_then(|v| v.as_str())
42            .ok_or_else(|| {
43                ExecutionError::RollbackFailed("Missing backup_path in restore action".to_string())
44            })?;
45
46        // Validate paths using PathResolver
47        let resolved_file_path = PathResolver::expand_home(Path::new(file_path))
48            .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid file path: {}", e)))?;
49
50        let resolved_backup_path = PathResolver::expand_home(Path::new(backup_path))
51            .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid backup path: {}", e)))?;
52
53        // Check if backup exists
54        if !resolved_backup_path.exists() {
55            return Err(ExecutionError::RollbackFailed(format!(
56                "Backup file not found: {}",
57                backup_path
58            )));
59        }
60
61        // Create parent directories if needed
62        if let Some(parent) = resolved_file_path.parent() {
63            std::fs::create_dir_all(parent).map_err(|e| {
64                ExecutionError::RollbackFailed(format!(
65                    "Failed to create parent directories for {}: {}",
66                    file_path, e
67                ))
68            })?;
69        }
70
71        // Restore the file from backup
72        std::fs::copy(&resolved_backup_path, &resolved_file_path).map_err(|e| {
73            ExecutionError::RollbackFailed(format!(
74                "Failed to restore file {} from backup: {}",
75                file_path, e
76            ))
77        })?;
78
79        let message = format!("Restored {} from backup", file_path);
80        info!("{}", message);
81        Ok(message)
82    }
83}
84
85/// Handles deletion of created files
86pub struct DeleteFileHandler;
87
88impl DeleteFileHandler {
89    /// Delete a created file
90    ///
91    /// # Arguments
92    /// * `action` - Rollback action containing file path
93    ///
94    /// # Returns
95    /// Success message if deletion succeeds
96    pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
97        debug!("Deleting created file");
98
99        // Extract file path from action data
100        let file_path = action
101            .data
102            .get("file_path")
103            .and_then(|v| v.as_str())
104            .ok_or_else(|| {
105                ExecutionError::RollbackFailed("Missing file_path in delete action".to_string())
106            })?;
107
108        // Validate path using PathResolver
109        let resolved_path = PathResolver::expand_home(Path::new(file_path))
110            .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid path: {}", e)))?;
111
112        // Check if file exists
113        if !resolved_path.exists() {
114            warn!(
115                file_path = %file_path,
116                "File to delete does not exist, skipping"
117            );
118            return Ok(format!("File {} already deleted", file_path));
119        }
120
121        // Delete the file
122        std::fs::remove_file(&resolved_path).map_err(|e| {
123            ExecutionError::RollbackFailed(format!("Failed to delete file {}: {}", file_path, e))
124        })?;
125
126        let message = format!("Deleted {}", file_path);
127        info!("{}", message);
128        Ok(message)
129    }
130}
131
132/// Handles execution of undo commands
133pub struct UndoCommandHandler;
134
135impl UndoCommandHandler {
136    /// Execute an undo command
137    ///
138    /// # Arguments
139    /// * `action` - Rollback action containing command and arguments
140    ///
141    /// # Returns
142    /// Success message if command succeeds
143    pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
144        debug!("Running undo command");
145
146        // Extract command and args from action data
147        let command = action
148            .data
149            .get("command")
150            .and_then(|v| v.as_str())
151            .ok_or_else(|| {
152                ExecutionError::RollbackFailed("Missing command in undo action".to_string())
153            })?;
154
155        let args: Vec<String> = action
156            .data
157            .get("args")
158            .and_then(|v| v.as_array())
159            .map(|arr| {
160                arr.iter()
161                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
162                    .collect()
163            })
164            .unwrap_or_default();
165
166        // Execute the undo command
167        let mut cmd = Command::new(command);
168        cmd.args(&args);
169
170        let output = cmd.output().map_err(|e| {
171            ExecutionError::RollbackFailed(format!(
172                "Failed to execute undo command {}: {}",
173                command, e
174            ))
175        })?;
176
177        if !output.status.success() {
178            let stderr = String::from_utf8_lossy(&output.stderr);
179            return Err(ExecutionError::RollbackFailed(format!(
180                "Undo command {} failed with exit code {:?}: {}",
181                command,
182                output.status.code(),
183                stderr
184            )));
185        }
186
187        let message = format!("Executed undo command: {}", command);
188        info!("{}", message);
189        Ok(message)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use serde_json::json;
197
198    #[test]
199    fn test_restore_file_handler_missing_file_path() {
200        let action = RollbackAction {
201            action_type: crate::models::RollbackType::RestoreFile,
202            data: json!({ "backup_path": "/tmp/backup.txt" }),
203        };
204
205        let result = RestoreFileHandler::handle(&action);
206        assert!(result.is_err());
207    }
208
209    #[test]
210    fn test_restore_file_handler_missing_backup_path() {
211        let action = RollbackAction {
212            action_type: crate::models::RollbackType::RestoreFile,
213            data: json!({ "file_path": "/tmp/test.txt" }),
214        };
215
216        let result = RestoreFileHandler::handle(&action);
217        assert!(result.is_err());
218    }
219
220    #[test]
221    fn test_delete_file_handler_missing_file_path() {
222        let action = RollbackAction {
223            action_type: crate::models::RollbackType::DeleteFile,
224            data: json!({}),
225        };
226
227        let result = DeleteFileHandler::handle(&action);
228        assert!(result.is_err());
229    }
230
231    #[test]
232    fn test_delete_file_handler_nonexistent_file() {
233        let action = RollbackAction {
234            action_type: crate::models::RollbackType::DeleteFile,
235            data: json!({ "file_path": "/tmp/nonexistent_file_12345.txt" }),
236        };
237
238        let result = DeleteFileHandler::handle(&action);
239        assert!(result.is_ok());
240        let message = result.unwrap();
241        assert!(message.contains("already deleted"));
242    }
243
244    #[test]
245    fn test_undo_command_handler_missing_command() {
246        let action = RollbackAction {
247            action_type: crate::models::RollbackType::RunCommand,
248            data: json!({ "args": [] }),
249        };
250
251        let result = UndoCommandHandler::handle(&action);
252        assert!(result.is_err());
253    }
254
255    #[test]
256    fn test_undo_command_handler_with_args() {
257        let action = RollbackAction {
258            action_type: crate::models::RollbackType::RunCommand,
259            data: json!({
260                "command": "echo",
261                "args": ["test"]
262            }),
263        };
264
265        let result = UndoCommandHandler::handle(&action);
266        assert!(result.is_ok());
267    }
268}