uv_migrator/utils/
file_ops.rs1use crate::error::{Error, Result};
2use log::{debug, info, warn};
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
9pub enum FileChange {
10 Created {
12 original_existed: bool,
13 original_content: Option<Vec<u8>>,
14 },
15 Renamed { source_path: PathBuf },
17}
18
19impl FileChange {
20 pub fn new_created() -> Self {
22 FileChange::Created {
23 original_existed: false,
24 original_content: None,
25 }
26 }
27
28 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 pub fn renamed(source_path: PathBuf) -> Self {
38 FileChange::Renamed { source_path }
39 }
40}
41
42pub struct FileTracker {
44 changes: HashMap<PathBuf, FileChange>,
46 restore_enabled: bool,
48 force_rollback: bool,
50}
51
52impl Default for FileTracker {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl FileTracker {
59 pub fn new() -> Self {
61 Self {
62 changes: HashMap::new(),
63 restore_enabled: true,
64 force_rollback: false,
65 }
66 }
67
68 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 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 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 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 pub fn force_rollback(&mut self) {
136 self.force_rollback = true;
137 }
138
139 pub fn rollback(&mut self) -> Result<()> {
141 info!("Rolling back file changes...");
142
143 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 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 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 #[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 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
236pub struct FileTrackerGuard {
238 inner: FileTracker,
239}
240
241impl Default for FileTrackerGuard {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247impl FileTrackerGuard {
248 pub fn new() -> Self {
250 Self {
251 inner: FileTracker::new(),
252 }
253 }
254
255 pub fn new_with_restore(restore_enabled: bool) -> Self {
257 Self {
258 inner: FileTracker::new_with_restore(restore_enabled),
259 }
260 }
261
262 pub fn track_file(&mut self, path: &Path) -> Result<()> {
264 self.inner.track_file(path)
265 }
266
267 pub fn track_rename(&mut self, source: &Path, target: &Path) -> Result<()> {
269 self.inner.track_rename(source, target)
270 }
271
272 pub fn force_rollback(&mut self) {
274 self.inner.force_rollback();
275 }
276}