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