ricecoder_execution/
rollback_actions.rs1use 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
15pub struct RestoreFileHandler;
17
18impl RestoreFileHandler {
19 pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
27 debug!("Restoring file from backup");
28
29 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 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 if !resolved_backup_path.exists() {
55 return Err(ExecutionError::RollbackFailed(format!(
56 "Backup file not found: {}",
57 backup_path
58 )));
59 }
60
61 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 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
85pub struct DeleteFileHandler;
87
88impl DeleteFileHandler {
89 pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
97 debug!("Deleting created file");
98
99 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 let resolved_path = PathResolver::expand_home(Path::new(file_path))
110 .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid path: {}", e)))?;
111
112 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 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
132pub struct UndoCommandHandler;
134
135impl UndoCommandHandler {
136 pub fn handle(action: &RollbackAction) -> ExecutionResult<String> {
144 debug!("Running undo command");
145
146 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 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}