1use crate::error::{ExecutionError, ExecutionResult};
7use crate::models::{ExecutionStep, RollbackAction, RollbackType};
8use ricecoder_storage::PathResolver;
9use std::path::Path;
10use std::process::Command;
11use tracing::{debug, error, info, warn};
12
13pub struct RollbackHandler {
18 rollback_actions: Vec<(String, RollbackAction)>,
20 in_progress: bool,
22}
23
24impl RollbackHandler {
25 pub fn new() -> Self {
27 Self {
28 rollback_actions: Vec::new(),
29 in_progress: false,
30 }
31 }
32
33 pub fn track_action(&mut self, step_id: String, action: RollbackAction) {
39 debug!(
40 step_id = %step_id,
41 action_type = ?action.action_type,
42 "Tracking rollback action"
43 );
44 self.rollback_actions.push((step_id, action));
45 }
46
47 pub fn track_step(&mut self, step: &ExecutionStep) {
51 if let Some(rollback_action) = &step.rollback_action {
52 self.track_action(step.id.clone(), rollback_action.clone());
53 }
54 }
55
56 pub fn execute_rollback(&mut self) -> ExecutionResult<Vec<RollbackResult>> {
64 if self.rollback_actions.is_empty() {
65 info!("No rollback actions to execute");
66 return Ok(Vec::new());
67 }
68
69 info!(
70 action_count = self.rollback_actions.len(),
71 "Starting rollback execution"
72 );
73
74 self.in_progress = true;
75 let mut results = Vec::new();
76
77 for (step_id, action) in self.rollback_actions.iter().rev() {
79 debug!(
80 step_id = %step_id,
81 action_type = ?action.action_type,
82 "Executing rollback action"
83 );
84
85 match self.execute_rollback_action(step_id, action) {
86 Ok(result) => {
87 info!(
88 step_id = %step_id,
89 "Rollback action completed successfully"
90 );
91 results.push(result);
92 }
93 Err(e) => {
94 error!(
95 step_id = %step_id,
96 error = %e,
97 "Rollback action failed"
98 );
99 self.in_progress = false;
100 return Err(ExecutionError::RollbackFailed(format!(
101 "Rollback failed for step {}: {}",
102 step_id, e
103 )));
104 }
105 }
106 }
107
108 self.in_progress = false;
109 info!(
110 completed_actions = results.len(),
111 "Rollback execution completed"
112 );
113
114 Ok(results)
115 }
116
117 pub fn execute_partial_rollback(
127 &mut self,
128 step_ids: &[String],
129 ) -> ExecutionResult<Vec<RollbackResult>> {
130 info!(
131 step_count = step_ids.len(),
132 "Starting partial rollback execution"
133 );
134
135 self.in_progress = true;
136 let mut results = Vec::new();
137
138 for (step_id, action) in self.rollback_actions.iter().rev() {
140 if step_ids.contains(step_id) {
141 debug!(
142 step_id = %step_id,
143 action_type = ?action.action_type,
144 "Executing partial rollback action"
145 );
146
147 match self.execute_rollback_action(step_id, action) {
148 Ok(result) => {
149 info!(
150 step_id = %step_id,
151 "Partial rollback action completed successfully"
152 );
153 results.push(result);
154 }
155 Err(e) => {
156 error!(
157 step_id = %step_id,
158 error = %e,
159 "Partial rollback action failed"
160 );
161 self.in_progress = false;
162 return Err(ExecutionError::RollbackFailed(format!(
163 "Partial rollback failed for step {}: {}",
164 step_id, e
165 )));
166 }
167 }
168 }
169 }
170
171 self.in_progress = false;
172 info!(
173 completed_actions = results.len(),
174 "Partial rollback execution completed"
175 );
176
177 Ok(results)
178 }
179
180 fn execute_rollback_action(
182 &self,
183 step_id: &str,
184 action: &RollbackAction,
185 ) -> ExecutionResult<RollbackResult> {
186 match action.action_type {
187 RollbackType::RestoreFile => self.handle_restore_file(step_id, action),
188 RollbackType::DeleteFile => self.handle_delete_file(step_id, action),
189 RollbackType::RunCommand => self.handle_run_command(step_id, action),
190 }
191 }
192
193 fn handle_restore_file(
195 &self,
196 step_id: &str,
197 action: &RollbackAction,
198 ) -> ExecutionResult<RollbackResult> {
199 debug!(step_id = %step_id, "Restoring file from backup");
200
201 let file_path = action
203 .data
204 .get("file_path")
205 .and_then(|v| v.as_str())
206 .ok_or_else(|| {
207 ExecutionError::RollbackFailed("Missing file_path in restore action".to_string())
208 })?;
209
210 let backup_path = action
211 .data
212 .get("backup_path")
213 .and_then(|v| v.as_str())
214 .ok_or_else(|| {
215 ExecutionError::RollbackFailed("Missing backup_path in restore action".to_string())
216 })?;
217
218 let resolved_file_path = PathResolver::expand_home(Path::new(file_path))
220 .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid file path: {}", e)))?;
221
222 let resolved_backup_path = PathResolver::expand_home(Path::new(backup_path))
223 .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid backup path: {}", e)))?;
224
225 if !resolved_backup_path.exists() {
227 return Err(ExecutionError::RollbackFailed(format!(
228 "Backup file not found: {}",
229 backup_path
230 )));
231 }
232
233 std::fs::copy(&resolved_backup_path, &resolved_file_path).map_err(|e| {
235 ExecutionError::RollbackFailed(format!(
236 "Failed to restore file {} from backup: {}",
237 file_path, e
238 ))
239 })?;
240
241 info!(
242 file_path = %file_path,
243 backup_path = %backup_path,
244 "File restored from backup"
245 );
246
247 Ok(RollbackResult {
248 step_id: step_id.to_string(),
249 action_type: RollbackType::RestoreFile,
250 success: true,
251 message: format!("Restored {} from backup", file_path),
252 })
253 }
254
255 fn handle_delete_file(
257 &self,
258 step_id: &str,
259 action: &RollbackAction,
260 ) -> ExecutionResult<RollbackResult> {
261 debug!(step_id = %step_id, "Deleting created file");
262
263 let file_path = action
265 .data
266 .get("file_path")
267 .and_then(|v| v.as_str())
268 .ok_or_else(|| {
269 ExecutionError::RollbackFailed("Missing file_path in delete action".to_string())
270 })?;
271
272 let resolved_path = PathResolver::expand_home(Path::new(file_path))
274 .map_err(|e| ExecutionError::RollbackFailed(format!("Invalid path: {}", e)))?;
275
276 if !resolved_path.exists() {
278 warn!(
279 file_path = %file_path,
280 "File to delete does not exist, skipping"
281 );
282 return Ok(RollbackResult {
283 step_id: step_id.to_string(),
284 action_type: RollbackType::DeleteFile,
285 success: true,
286 message: format!("File {} already deleted", file_path),
287 });
288 }
289
290 std::fs::remove_file(&resolved_path).map_err(|e| {
292 ExecutionError::RollbackFailed(format!("Failed to delete file {}: {}", file_path, e))
293 })?;
294
295 info!(file_path = %file_path, "File deleted successfully");
296
297 Ok(RollbackResult {
298 step_id: step_id.to_string(),
299 action_type: RollbackType::DeleteFile,
300 success: true,
301 message: format!("Deleted {}", file_path),
302 })
303 }
304
305 fn handle_run_command(
307 &self,
308 step_id: &str,
309 action: &RollbackAction,
310 ) -> ExecutionResult<RollbackResult> {
311 debug!(step_id = %step_id, "Running undo command");
312
313 let command = action
315 .data
316 .get("command")
317 .and_then(|v| v.as_str())
318 .ok_or_else(|| {
319 ExecutionError::RollbackFailed("Missing command in undo action".to_string())
320 })?;
321
322 let args: Vec<String> = action
323 .data
324 .get("args")
325 .and_then(|v| v.as_array())
326 .map(|arr| {
327 arr.iter()
328 .filter_map(|v| v.as_str().map(|s| s.to_string()))
329 .collect()
330 })
331 .unwrap_or_default();
332
333 let mut cmd = Command::new(command);
335 cmd.args(&args);
336
337 let output = cmd.output().map_err(|e| {
338 ExecutionError::RollbackFailed(format!(
339 "Failed to execute undo command {}: {}",
340 command, e
341 ))
342 })?;
343
344 if !output.status.success() {
345 let stderr = String::from_utf8_lossy(&output.stderr);
346 return Err(ExecutionError::RollbackFailed(format!(
347 "Undo command {} failed with exit code {:?}: {}",
348 command,
349 output.status.code(),
350 stderr
351 )));
352 }
353
354 info!(
355 command = %command,
356 "Undo command executed successfully"
357 );
358
359 Ok(RollbackResult {
360 step_id: step_id.to_string(),
361 action_type: RollbackType::RunCommand,
362 success: true,
363 message: format!("Executed undo command: {}", command),
364 })
365 }
366
367 pub fn verify_completeness(&self) -> bool {
375 if self.in_progress {
376 warn!("Rollback verification requested while rollback is in progress");
377 return false;
378 }
379
380 debug!("Rollback completeness verification passed");
387 true
388 }
389
390 pub fn action_count(&self) -> usize {
392 self.rollback_actions.len()
393 }
394
395 pub fn is_in_progress(&self) -> bool {
397 self.in_progress
398 }
399
400 pub fn clear(&mut self) {
402 self.rollback_actions.clear();
403 debug!("Cleared all tracked rollback actions");
404 }
405}
406
407impl Default for RollbackHandler {
408 fn default() -> Self {
409 Self::new()
410 }
411}
412
413#[derive(Debug, Clone)]
415pub struct RollbackResult {
416 pub step_id: String,
418 pub action_type: RollbackType,
420 pub success: bool,
422 pub message: String,
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use serde_json::json;
430
431 #[test]
432 fn test_create_rollback_handler() {
433 let handler = RollbackHandler::new();
434 assert_eq!(handler.action_count(), 0);
435 assert!(!handler.is_in_progress());
436 }
437
438 #[test]
439 fn test_track_action() {
440 let mut handler = RollbackHandler::new();
441 let action = RollbackAction {
442 action_type: RollbackType::DeleteFile,
443 data: json!({ "file_path": "/tmp/test.txt" }),
444 };
445
446 handler.track_action("step-1".to_string(), action);
447 assert_eq!(handler.action_count(), 1);
448 }
449
450 #[test]
451 fn test_clear_actions() {
452 let mut handler = RollbackHandler::new();
453 let action = RollbackAction {
454 action_type: RollbackType::DeleteFile,
455 data: json!({ "file_path": "/tmp/test.txt" }),
456 };
457
458 handler.track_action("step-1".to_string(), action);
459 assert_eq!(handler.action_count(), 1);
460
461 handler.clear();
462 assert_eq!(handler.action_count(), 0);
463 }
464
465 #[test]
466 fn test_verify_completeness() {
467 let handler = RollbackHandler::new();
468 assert!(handler.verify_completeness());
469 }
470
471 #[test]
472 fn test_verify_completeness_in_progress() {
473 let mut handler = RollbackHandler::new();
474 handler.in_progress = true;
475 assert!(!handler.verify_completeness());
476 }
477
478 #[test]
479 fn test_execute_rollback_empty() {
480 let mut handler = RollbackHandler::new();
481 let result = handler.execute_rollback();
482 assert!(result.is_ok());
483 assert_eq!(result.unwrap().len(), 0);
484 }
485
486 #[test]
487 fn test_execute_partial_rollback_empty() {
488 let mut handler = RollbackHandler::new();
489 let result = handler.execute_partial_rollback(&[]);
490 assert!(result.is_ok());
491 assert_eq!(result.unwrap().len(), 0);
492 }
493
494 #[test]
495 fn test_rollback_result_creation() {
496 let result = RollbackResult {
497 step_id: "step-1".to_string(),
498 action_type: RollbackType::DeleteFile,
499 success: true,
500 message: "File deleted".to_string(),
501 };
502
503 assert_eq!(result.step_id, "step-1");
504 assert_eq!(result.action_type, RollbackType::DeleteFile);
505 assert!(result.success);
506 }
507}