client_core/patch_executor/
file_operations.rs1use super::error::{PatchExecutorError, Result};
7use fs_extra::dir;
8use remove_dir_all::remove_dir_all;
9use std::path::{Path, PathBuf};
10use tempfile::{NamedTempFile, TempDir};
11use tokio::fs;
12use tracing::{debug, info, warn};
13use walkdir::WalkDir;
14
15pub struct FileOperationExecutor {
17 work_dir: PathBuf,
19 backup_dir: Option<TempDir>,
21 patch_source: Option<PathBuf>,
23}
24
25impl FileOperationExecutor {
26 pub fn new(work_dir: PathBuf) -> Result<Self> {
28 if !work_dir.exists() {
29 return Err(PatchExecutorError::path_error(format!(
30 "Working directory does not exist: {work_dir:?}"
31 )));
32 }
33
34 debug!("Creating file operation executor, working directory: {:?}", work_dir);
35
36 Ok(Self {
37 work_dir,
38 backup_dir: None,
39 patch_source: None,
40 })
41 }
42
43 pub fn enable_backup(&mut self) -> Result<()> {
45 self.backup_dir = Some(TempDir::new()?);
46 info!("File operation backup mode enabled");
47 Ok(())
48 }
49
50 pub fn set_patch_source(&mut self, patch_source: &Path) -> Result<()> {
52 if !patch_source.exists() {
53 return Err(PatchExecutorError::path_error(format!(
54 "Patch source directory does not exist: {patch_source:?}"
55 )));
56 }
57
58 self.patch_source = Some(patch_source.to_owned());
59 debug!("Setting patch source directory: {:?}", patch_source);
60 Ok(())
61 }
62
63 pub async fn replace_files(&self, files: &[String]) -> Result<()> {
65 info!("Starting to replace {} files", files.len());
66
67 for file_path in files {
68 self.replace_single_file(file_path).await?;
69 }
70
71 info!("File replacement completed");
72 Ok(())
73 }
74
75 pub async fn replace_directories(&self, directories: &[String]) -> Result<()> {
77 info!("Starting to replace {} directories", directories.len());
78
79 for dir_path in directories {
80 self.replace_single_directory(dir_path).await?;
81 }
82
83 info!("Directory replacement completed");
84 Ok(())
85 }
86
87 pub async fn delete_items(&self, items: &[String]) -> Result<()> {
89 info!("Starting to delete {} items", items.len());
90
91 for item_path in items {
92 self.delete_single_item(item_path).await?;
93 }
94
95 info!("Delete operation completed");
96 Ok(())
97 }
98
99 async fn replace_single_file(&self, file_path: &str) -> Result<()> {
101 let target_path = self.work_dir.join(file_path);
102
103 let source_path = self.get_patch_source_path(file_path)?;
105
106 if let Some(backup_dir) = &self.backup_dir {
108 if target_path.exists() {
109 let backup_path = backup_dir.path().join(file_path);
110 if let Some(parent) = backup_path.parent() {
111 fs::create_dir_all(parent).await?;
112 }
113 fs::copy(&target_path, &backup_path).await?;
114 debug!("Backed up file: {} -> {:?}", file_path, backup_path);
115 }
116 }
117
118 self.atomic_file_replace(&source_path, &target_path).await?;
120
121 info!("File replaced: {}", file_path);
122 Ok(())
123 }
124
125 async fn replace_single_directory(&self, dir_path: &str) -> Result<()> {
127 let target_path = self.work_dir.join(dir_path);
128
129 let source_path = self.get_patch_source_path(dir_path)?;
131
132 if let Some(backup_dir) = &self.backup_dir {
134 if target_path.exists() {
135 let backup_path = backup_dir.path().join(dir_path);
136 self.backup_directory(&target_path, &backup_path).await?;
137 debug!("Backed up directory: {} -> {:?}", dir_path, backup_path);
138 }
139 }
140
141 if target_path.exists() {
143 self.safe_remove_directory(&target_path).await?;
144 }
145
146 self.copy_directory(&source_path, &target_path).await?;
148
149 info!("Directory replaced: {}", dir_path);
150 Ok(())
151 }
152
153 async fn delete_single_item(&self, item_path: &str) -> Result<()> {
155 let target_path = self.work_dir.join(item_path);
156
157 if !target_path.exists() {
158 warn!("Delete target does not exist, skipping: {}", item_path);
159 return Ok(());
160 }
161
162 if let Some(backup_dir) = &self.backup_dir {
164 let backup_path = backup_dir.path().join(item_path);
165 if target_path.is_dir() {
166 self.backup_directory(&target_path, &backup_path).await?;
167 } else {
168 if let Some(parent) = backup_path.parent() {
169 fs::create_dir_all(parent).await?;
170 }
171 fs::copy(&target_path, &backup_path).await?;
172 }
173 debug!("Backed up item for deletion: {} -> {:?}", item_path, backup_path);
174 }
175
176 if target_path.is_dir() {
178 self.safe_remove_directory(&target_path).await?;
179 } else {
180 fs::remove_file(&target_path).await?;
181 }
182
183 info!("Deleted: {}", item_path);
184 Ok(())
185 }
186
187 fn get_patch_source_path(&self, relative_path: &str) -> Result<PathBuf> {
189 let patch_source = self
190 .patch_source
191 .as_ref()
192 .ok_or(PatchExecutorError::PatchSourceNotSet)?;
193
194 let source_path = patch_source.join(relative_path);
195
196 if !source_path.exists() {
197 return Err(PatchExecutorError::path_error(format!(
198 "Patch source file does not exist: {source_path:?}"
199 )));
200 }
201
202 Ok(source_path)
203 }
204
205 async fn atomic_file_replace(&self, source: &Path, target: &Path) -> Result<()> {
207 if let Some(parent) = target.parent() {
209 fs::create_dir_all(parent).await?;
210 }
211
212 let temp_file = NamedTempFile::new_in(target.parent().unwrap_or_else(|| Path::new(".")))?;
214
215 let source_content = fs::read(source).await?;
217 fs::write(temp_file.path(), source_content).await?;
218
219 temp_file.persist(target)?;
221
222 debug!("Atomic replacement completed: {:?} -> {:?}", source, target);
223 Ok(())
224 }
225
226 async fn safe_remove_directory(&self, path: &Path) -> Result<()> {
228 let path_clone = path.to_owned();
229 tokio::task::spawn_blocking(move || remove_dir_all(&path_clone))
230 .await
231 .map_err(|e| PatchExecutorError::custom(format!("Delete directory task failed: {e}")))??;
232
233 debug!("Safely deleted directory: {:?}", path);
234 Ok(())
235 }
236
237 async fn copy_directory(&self, source: &Path, target: &Path) -> Result<()> {
239 let source_clone = source.to_owned();
240 let target_clone = target.to_owned();
241
242 tokio::task::spawn_blocking(move || {
243 let options = dir::CopyOptions::new().overwrite(true).copy_inside(true);
244
245 if let Some(parent) = target_clone.parent() {
247 std::fs::create_dir_all(parent)
248 .map_err(|e| PatchExecutorError::custom(format!("Failed to create target parent directory: {e}")))?;
249 }
250
251 if !target_clone.exists() {
253 std::fs::create_dir_all(&target_clone)
254 .map_err(|e| PatchExecutorError::custom(format!("Failed to create target directory: {e}")))?;
255 }
256
257 dir::copy(
259 &source_clone,
260 target_clone.parent().unwrap_or(&target_clone),
261 &options,
262 )
263 .map_err(|e| PatchExecutorError::custom(format!("Directory copy failed: {e}")))?;
264
265 Ok::<(), PatchExecutorError>(())
266 })
267 .await
268 .map_err(|e| PatchExecutorError::custom(format!("Failed to copy directory task: {e}")))??;
269
270 debug!("Directory copied: {:?} -> {:?}", source, target);
271 Ok(())
272 }
273
274 async fn backup_directory(&self, source: &Path, backup: &Path) -> Result<()> {
276 if let Some(parent) = backup.parent() {
277 fs::create_dir_all(parent).await?;
278 }
279
280 self.copy_directory(source, backup).await?;
281 debug!("Directory backed up: {:?} -> {:?}", source, backup);
282 Ok(())
283 }
284
285 pub async fn rollback(&self) -> Result<()> {
287 if let Some(backup_dir) = &self.backup_dir {
288 warn!("Starting file operation rollback...");
289
290 let backup_path = backup_dir.path().to_owned();
292 let work_dir = self.work_dir.clone();
293
294 tokio::task::spawn_blocking(move || {
295 for entry in WalkDir::new(&backup_path) {
296 let entry = entry.map_err(|e| {
297 PatchExecutorError::custom(format!("Failed to traverse backup directory: {e}"))
298 })?;
299
300 let backup_file_path = entry.path();
301 if backup_file_path.is_file() {
302 let relative_path =
304 backup_file_path.strip_prefix(&backup_path).map_err(|e| {
305 PatchExecutorError::custom(format!("Failed to calculate relative path: {e}"))
306 })?;
307
308 let target_path = work_dir.join(relative_path);
309
310 if let Some(parent) = target_path.parent() {
312 std::fs::create_dir_all(parent).map_err(|e| {
313 PatchExecutorError::custom(format!("Failed to create rollback target directory: {e}"))
314 })?;
315 }
316
317 std::fs::copy(backup_file_path, &target_path).map_err(|e| {
319 PatchExecutorError::custom(format!("Failed to restore file: {e}"))
320 })?;
321
322 debug!("Restoring file: {:?} -> {:?}", backup_file_path, target_path);
323 }
324 }
325
326 Ok::<(), PatchExecutorError>(())
327 })
328 .await
329 .map_err(|e| PatchExecutorError::custom(format!("Rollback task failed: {e}")))??;
330
331 info!("File operation rollback completed");
332 } else {
333 return Err(PatchExecutorError::BackupNotEnabled);
334 }
335
336 Ok(())
337 }
338
339 pub fn work_dir(&self) -> &Path {
341 &self.work_dir
342 }
343
344 pub fn is_backup_enabled(&self) -> bool {
346 self.backup_dir.is_some()
347 }
348
349 pub fn patch_source(&self) -> Option<&Path> {
351 self.patch_source.as_deref()
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use tempfile::TempDir;
359 use tokio::fs;
360
361 #[tokio::test]
362 async fn test_file_operation_executor_creation() {
363 let temp_dir = TempDir::new().unwrap();
364 let executor = FileOperationExecutor::new(temp_dir.path().to_owned());
365 assert!(executor.is_ok());
366 }
367
368 #[tokio::test]
369 async fn test_enable_backup() {
370 let temp_dir = TempDir::new().unwrap();
371 let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
372
373 assert!(!executor.is_backup_enabled());
374 let result = executor.enable_backup();
375 assert!(result.is_ok());
376 assert!(executor.is_backup_enabled());
377 }
378
379 #[tokio::test]
380 async fn test_invalid_work_dir() {
381 let invalid_path = PathBuf::from("/nonexistent/path");
382 let executor = FileOperationExecutor::new(invalid_path);
383 assert!(executor.is_err());
384 }
385
386 #[tokio::test]
387 async fn test_set_patch_source() {
388 let temp_dir = TempDir::new().unwrap();
389 let patch_source_dir = TempDir::new().unwrap();
390
391 let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
392 let result = executor.set_patch_source(patch_source_dir.path());
393 assert!(result.is_ok());
394 assert_eq!(executor.patch_source(), Some(patch_source_dir.path()));
395 }
396
397 #[tokio::test]
398 async fn test_atomic_file_replace() {
399 let temp_dir = TempDir::new().unwrap();
400 let executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
401
402 let source_file = temp_dir.path().join("source.txt");
404 let content = "test content";
405 fs::write(&source_file, content).await.unwrap();
406
407 let target_file = temp_dir.path().join("target.txt");
409
410 executor
412 .atomic_file_replace(&source_file, &target_file)
413 .await
414 .unwrap();
415
416 let target_content = fs::read_to_string(&target_file).await.unwrap();
418 assert_eq!(target_content, content);
419 }
420
421 #[tokio::test]
422 async fn test_file_replacement_with_backup() {
423 let temp_dir = TempDir::new().unwrap();
424 let patch_source_dir = TempDir::new().unwrap();
425
426 let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
427 executor.enable_backup().unwrap();
428 executor.set_patch_source(patch_source_dir.path()).unwrap();
429
430 let original_file = temp_dir.path().join("test.txt");
432 let original_content = "original content";
433 fs::write(&original_file, original_content).await.unwrap();
434
435 let patch_file = patch_source_dir.path().join("test.txt");
437 let patch_content = "new content";
438 fs::write(&patch_file, patch_content).await.unwrap();
439
440 executor
442 .replace_files(&["test.txt".to_string()])
443 .await
444 .unwrap();
445
446 let new_content = fs::read_to_string(&original_file).await.unwrap();
448 assert_eq!(new_content, patch_content);
449
450 executor.rollback().await.unwrap();
452
453 let restored_content = fs::read_to_string(&original_file).await.unwrap();
455 assert_eq!(restored_content, original_content);
456 }
457
458 #[tokio::test]
459 async fn test_directory_operations() {
460 let temp_dir = TempDir::new().unwrap();
461 let patch_source_dir = TempDir::new().unwrap();
462
463 let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
464 executor.enable_backup().unwrap();
465 executor.set_patch_source(patch_source_dir.path()).unwrap();
466
467 let original_dir = temp_dir.path().join("testdir");
469 fs::create_dir_all(&original_dir).await.unwrap();
470 fs::write(original_dir.join("file1.txt"), "original file1")
471 .await
472 .unwrap();
473
474 let patch_dir = patch_source_dir.path().join("testdir");
476 fs::create_dir_all(&patch_dir).await.unwrap();
477 fs::write(patch_dir.join("file2.txt"), "new file2")
478 .await
479 .unwrap();
480
481 executor
483 .replace_directories(&["testdir".to_string()])
484 .await
485 .unwrap();
486
487 assert!(!original_dir.join("file1.txt").exists());
489 assert!(original_dir.join("file2.txt").exists());
490 let new_content = fs::read_to_string(original_dir.join("file2.txt"))
491 .await
492 .unwrap();
493 assert_eq!(new_content, "new file2");
494 }
495
496 #[tokio::test]
497 async fn test_delete_operations() {
498 let temp_dir = TempDir::new().unwrap();
499 let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
500 executor.enable_backup().unwrap();
501
502 let test_file = temp_dir.path().join("to_delete.txt");
504 fs::write(&test_file, "delete me").await.unwrap();
505
506 executor
508 .delete_items(&["to_delete.txt".to_string()])
509 .await
510 .unwrap();
511
512 assert!(!test_file.exists());
514
515 executor.rollback().await.unwrap();
517
518 assert!(test_file.exists());
520 let restored_content = fs::read_to_string(&test_file).await.unwrap();
521 assert_eq!(restored_content, "delete me");
522 }
523}