uv_migrator/utils/
file_ops.rs

1use crate::error::{Error, Result};
2use log::{debug, info, warn};
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Represents a file change that can be tracked for potential rollback
8#[derive(Debug, Clone)]
9pub enum FileChange {
10    /// File was created (contains its content for potential rollback)
11    Created {
12        original_existed: bool,
13        original_content: Option<Vec<u8>>,
14    },
15    /// File was renamed (contains source path for potential rollback)
16    Renamed { source_path: PathBuf },
17}
18
19impl FileChange {
20    /// Creates a new FileChange for a created file
21    pub fn new_created() -> Self {
22        FileChange::Created {
23            original_existed: false,
24            original_content: None,
25        }
26    }
27
28    /// Creates a new FileChange for a created file, storing original content for rollback
29    pub fn created_with_content(content: Vec<u8>) -> Self {
30        FileChange::Created {
31            original_existed: true,
32            original_content: Some(content),
33        }
34    }
35
36    /// Creates a new FileChange for a renamed file
37    pub fn renamed(source_path: PathBuf) -> Self {
38        FileChange::Renamed { source_path }
39    }
40}
41
42/// Tracks file changes and provides rollback functionality
43pub struct FileTracker {
44    /// Map of file paths to their tracked changes
45    changes: HashMap<PathBuf, FileChange>,
46    /// Whether automatic restore on drop is enabled
47    restore_enabled: bool,
48    /// Whether to force rollback regardless of restore_enabled
49    force_rollback: bool,
50}
51
52impl Default for FileTracker {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl FileTracker {
59    /// Creates a new FileTracker with restore on drop enabled
60    pub fn new() -> Self {
61        Self {
62            changes: HashMap::new(),
63            restore_enabled: true,
64            force_rollback: false,
65        }
66    }
67
68    /// Creates a new FileTracker with restore on drop configurable
69    pub fn new_with_restore(restore_enabled: bool) -> Self {
70        Self {
71            changes: HashMap::new(),
72            restore_enabled,
73            force_rollback: false,
74        }
75    }
76
77    /// Starts tracking a file
78    pub fn track_file(&mut self, path: &Path) -> Result<()> {
79        debug!("Tracking file: {}", path.display());
80
81        if self.changes.contains_key(path) {
82            debug!("File already tracked: {}", path.display());
83            return Ok(());
84        }
85
86        // If the file already exists, store its content for potential rollback
87        if path.exists() {
88            let content = fs::read(path).map_err(|e| Error::FileOperation {
89                path: path.to_path_buf(),
90                message: format!("Failed to read file content: {}", e),
91            })?;
92
93            self.changes.insert(
94                path.to_path_buf(),
95                FileChange::created_with_content(content),
96            );
97        } else {
98            self.changes
99                .insert(path.to_path_buf(), FileChange::new_created());
100        }
101
102        info!("Started tracking file: {}", path.display());
103        Ok(())
104    }
105
106    /// Tracks a file rename operation
107    pub fn track_rename(&mut self, source: &Path, target: &Path) -> Result<()> {
108        debug!(
109            "Tracking file rename: {} -> {}",
110            source.display(),
111            target.display()
112        );
113
114        if !source.exists() {
115            return Err(Error::FileOperation {
116                path: source.to_path_buf(),
117                message: "Source file doesn't exist".to_string(),
118            });
119        }
120
121        self.changes.insert(
122            target.to_path_buf(),
123            FileChange::renamed(source.to_path_buf()),
124        );
125
126        info!(
127            "Tracked rename operation: {} -> {}",
128            source.display(),
129            target.display()
130        );
131        Ok(())
132    }
133
134    /// Force rollback of tracked changes
135    pub fn force_rollback(&mut self) {
136        self.force_rollback = true;
137    }
138
139    /// Rollback all tracked changes
140    pub fn rollback(&mut self) -> Result<()> {
141        info!("Rolling back file changes...");
142
143        // Process file changes in reverse order
144        let paths: Vec<PathBuf> = self.changes.keys().cloned().collect();
145        for path in paths.iter().rev() {
146            if let Some(change) = self.changes.get(path) {
147                match change {
148                    FileChange::Created {
149                        original_existed,
150                        original_content,
151                    } => {
152                        if *original_existed {
153                            if let Some(content) = original_content {
154                                fs::write(path, content).map_err(|e| Error::FileOperation {
155                                    path: path.to_path_buf(),
156                                    message: format!("Failed to restore file content: {}", e),
157                                })?;
158                                info!("Restored original content to {}", path.display());
159                            }
160                        } else if path.exists() {
161                            fs::remove_file(path).map_err(|e| Error::FileOperation {
162                                path: path.to_path_buf(),
163                                message: format!("Failed to remove file: {}", e),
164                            })?;
165                            info!("Removed created file: {}", path.display());
166                        }
167                    }
168                    FileChange::Renamed { source_path } => {
169                        if path.exists() {
170                            if source_path.exists() {
171                                // Both files exist, need to move content
172                                let content = fs::read(path).map_err(|e| Error::FileOperation {
173                                    path: path.to_path_buf(),
174                                    message: format!("Failed to read renamed file: {}", e),
175                                })?;
176                                fs::write(source_path, content).map_err(|e| {
177                                    Error::FileOperation {
178                                        path: source_path.to_path_buf(),
179                                        message: format!("Failed to restore renamed file: {}", e),
180                                    }
181                                })?;
182                                fs::remove_file(path).map_err(|e| Error::FileOperation {
183                                    path: path.to_path_buf(),
184                                    message: format!("Failed to remove renamed file: {}", e),
185                                })?;
186                            } else {
187                                // Simple rename back
188                                fs::rename(path, source_path).map_err(|e| {
189                                    Error::FileOperation {
190                                        path: path.to_path_buf(),
191                                        message: format!(
192                                            "Failed to rename back to {}: {}",
193                                            source_path.display(),
194                                            e
195                                        ),
196                                    }
197                                })?;
198                            }
199                            info!(
200                                "Renamed file back: {} -> {}",
201                                path.display(),
202                                source_path.display()
203                            );
204                        }
205                    }
206                }
207            }
208        }
209
210        self.changes.clear();
211        info!("Rollback completed successfully");
212        Ok(())
213    }
214
215    /// Clear tracked changes without rollback
216    #[allow(dead_code)]
217    pub fn clear(&mut self) {
218        self.changes.clear();
219    }
220}
221
222impl Drop for FileTracker {
223    fn drop(&mut self) {
224        // Only perform rollback if force_rollback is true and restore_enabled is true
225        if self.force_rollback && self.restore_enabled && !self.changes.is_empty() {
226            match self.rollback() {
227                Ok(_) => {}
228                Err(e) => {
229                    warn!("Error during automatic rollback: {}", e);
230                }
231            }
232        }
233    }
234}
235
236/// A guard wrapper around FileTracker that simplifies working with tracked files
237pub struct FileTrackerGuard {
238    inner: FileTracker,
239}
240
241impl Default for FileTrackerGuard {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247impl FileTrackerGuard {
248    /// Creates a new FileTrackerGuard with restore on drop enabled
249    pub fn new() -> Self {
250        Self {
251            inner: FileTracker::new(),
252        }
253    }
254
255    /// Creates a new FileTrackerGuard with restore on drop configurable
256    pub fn new_with_restore(restore_enabled: bool) -> Self {
257        Self {
258            inner: FileTracker::new_with_restore(restore_enabled),
259        }
260    }
261
262    /// Starts tracking a file
263    pub fn track_file(&mut self, path: &Path) -> Result<()> {
264        self.inner.track_file(path)
265    }
266
267    /// Tracks a file rename operation
268    pub fn track_rename(&mut self, source: &Path, target: &Path) -> Result<()> {
269        self.inner.track_rename(source, target)
270    }
271
272    /// Force rollback of tracked changes
273    pub fn force_rollback(&mut self) {
274        self.inner.force_rollback();
275    }
276}