use super::error::{PatchExecutorError, Result};
use fs_extra::dir;
use remove_dir_all::remove_dir_all;
use std::path::{Path, PathBuf};
use tempfile::{NamedTempFile, TempDir};
use tokio::fs;
use tracing::{debug, info, warn};
use walkdir::WalkDir;
pub struct FileOperationExecutor {
work_dir: PathBuf,
backup_dir: Option<TempDir>,
patch_source: Option<PathBuf>,
}
impl FileOperationExecutor {
pub fn new(work_dir: PathBuf) -> Result<Self> {
if !work_dir.exists() {
return Err(PatchExecutorError::path_error(format!(
"Working directory does not exist: {work_dir:?}"
)));
}
debug!("Creating file operation executor, working directory: {:?}", work_dir);
Ok(Self {
work_dir,
backup_dir: None,
patch_source: None,
})
}
pub fn enable_backup(&mut self) -> Result<()> {
self.backup_dir = Some(TempDir::new()?);
info!("File operation backup mode enabled");
Ok(())
}
pub fn set_patch_source(&mut self, patch_source: &Path) -> Result<()> {
if !patch_source.exists() {
return Err(PatchExecutorError::path_error(format!(
"Patch source directory does not exist: {patch_source:?}"
)));
}
self.patch_source = Some(patch_source.to_owned());
debug!("Setting patch source directory: {:?}", patch_source);
Ok(())
}
pub async fn replace_files(&self, files: &[String]) -> Result<()> {
info!("Starting to replace {} files", files.len());
for file_path in files {
self.replace_single_file(file_path).await?;
}
info!("File replacement completed");
Ok(())
}
pub async fn replace_directories(&self, directories: &[String]) -> Result<()> {
info!("Starting to replace {} directories", directories.len());
for dir_path in directories {
self.replace_single_directory(dir_path).await?;
}
info!("Directory replacement completed");
Ok(())
}
pub async fn delete_items(&self, items: &[String]) -> Result<()> {
info!("Starting to delete {} items", items.len());
for item_path in items {
self.delete_single_item(item_path).await?;
}
info!("Delete operation completed");
Ok(())
}
async fn replace_single_file(&self, file_path: &str) -> Result<()> {
let target_path = self.work_dir.join(file_path);
let source_path = self.get_patch_source_path(file_path)?;
if let Some(backup_dir) = &self.backup_dir {
if target_path.exists() {
let backup_path = backup_dir.path().join(file_path);
if let Some(parent) = backup_path.parent() {
fs::create_dir_all(parent).await?;
}
fs::copy(&target_path, &backup_path).await?;
debug!("Backed up file: {} -> {:?}", file_path, backup_path);
}
}
self.atomic_file_replace(&source_path, &target_path).await?;
info!("File replaced: {}", file_path);
Ok(())
}
async fn replace_single_directory(&self, dir_path: &str) -> Result<()> {
let target_path = self.work_dir.join(dir_path);
let source_path = self.get_patch_source_path(dir_path)?;
if let Some(backup_dir) = &self.backup_dir {
if target_path.exists() {
let backup_path = backup_dir.path().join(dir_path);
self.backup_directory(&target_path, &backup_path).await?;
debug!("Backed up directory: {} -> {:?}", dir_path, backup_path);
}
}
if target_path.exists() {
self.safe_remove_directory(&target_path).await?;
}
self.copy_directory(&source_path, &target_path).await?;
info!("Directory replaced: {}", dir_path);
Ok(())
}
async fn delete_single_item(&self, item_path: &str) -> Result<()> {
let target_path = self.work_dir.join(item_path);
if !target_path.exists() {
warn!("Delete target does not exist, skipping: {}", item_path);
return Ok(());
}
if let Some(backup_dir) = &self.backup_dir {
let backup_path = backup_dir.path().join(item_path);
if target_path.is_dir() {
self.backup_directory(&target_path, &backup_path).await?;
} else {
if let Some(parent) = backup_path.parent() {
fs::create_dir_all(parent).await?;
}
fs::copy(&target_path, &backup_path).await?;
}
debug!("Backed up item for deletion: {} -> {:?}", item_path, backup_path);
}
if target_path.is_dir() {
self.safe_remove_directory(&target_path).await?;
} else {
fs::remove_file(&target_path).await?;
}
info!("Deleted: {}", item_path);
Ok(())
}
fn get_patch_source_path(&self, relative_path: &str) -> Result<PathBuf> {
let patch_source = self
.patch_source
.as_ref()
.ok_or(PatchExecutorError::PatchSourceNotSet)?;
let source_path = patch_source.join(relative_path);
if !source_path.exists() {
return Err(PatchExecutorError::path_error(format!(
"Patch source file does not exist: {source_path:?}"
)));
}
Ok(source_path)
}
async fn atomic_file_replace(&self, source: &Path, target: &Path) -> Result<()> {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).await?;
}
let temp_file = NamedTempFile::new_in(target.parent().unwrap_or_else(|| Path::new(".")))?;
let source_content = fs::read(source).await?;
fs::write(temp_file.path(), source_content).await?;
temp_file.persist(target)?;
debug!("Atomic replacement completed: {:?} -> {:?}", source, target);
Ok(())
}
async fn safe_remove_directory(&self, path: &Path) -> Result<()> {
let path_clone = path.to_owned();
tokio::task::spawn_blocking(move || remove_dir_all(&path_clone))
.await
.map_err(|e| PatchExecutorError::custom(format!("Delete directory task failed: {e}")))??;
debug!("Safely deleted directory: {:?}", path);
Ok(())
}
async fn copy_directory(&self, source: &Path, target: &Path) -> Result<()> {
let source_clone = source.to_owned();
let target_clone = target.to_owned();
tokio::task::spawn_blocking(move || {
let options = dir::CopyOptions::new().overwrite(true).copy_inside(true);
if let Some(parent) = target_clone.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| PatchExecutorError::custom(format!("Failed to create target parent directory: {e}")))?;
}
if !target_clone.exists() {
std::fs::create_dir_all(&target_clone)
.map_err(|e| PatchExecutorError::custom(format!("Failed to create target directory: {e}")))?;
}
dir::copy(
&source_clone,
target_clone.parent().unwrap_or(&target_clone),
&options,
)
.map_err(|e| PatchExecutorError::custom(format!("Directory copy failed: {e}")))?;
Ok::<(), PatchExecutorError>(())
})
.await
.map_err(|e| PatchExecutorError::custom(format!("Failed to copy directory task: {e}")))??;
debug!("Directory copied: {:?} -> {:?}", source, target);
Ok(())
}
async fn backup_directory(&self, source: &Path, backup: &Path) -> Result<()> {
if let Some(parent) = backup.parent() {
fs::create_dir_all(parent).await?;
}
self.copy_directory(source, backup).await?;
debug!("Directory backed up: {:?} -> {:?}", source, backup);
Ok(())
}
pub async fn rollback(&self) -> Result<()> {
if let Some(backup_dir) = &self.backup_dir {
warn!("Starting file operation rollback...");
let backup_path = backup_dir.path().to_owned();
let work_dir = self.work_dir.clone();
tokio::task::spawn_blocking(move || {
for entry in WalkDir::new(&backup_path) {
let entry = entry.map_err(|e| {
PatchExecutorError::custom(format!("Failed to traverse backup directory: {e}"))
})?;
let backup_file_path = entry.path();
if backup_file_path.is_file() {
let relative_path =
backup_file_path.strip_prefix(&backup_path).map_err(|e| {
PatchExecutorError::custom(format!("Failed to calculate relative path: {e}"))
})?;
let target_path = work_dir.join(relative_path);
if let Some(parent) = target_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
PatchExecutorError::custom(format!("Failed to create rollback target directory: {e}"))
})?;
}
std::fs::copy(backup_file_path, &target_path).map_err(|e| {
PatchExecutorError::custom(format!("Failed to restore file: {e}"))
})?;
debug!("Restoring file: {:?} -> {:?}", backup_file_path, target_path);
}
}
Ok::<(), PatchExecutorError>(())
})
.await
.map_err(|e| PatchExecutorError::custom(format!("Rollback task failed: {e}")))??;
info!("File operation rollback completed");
} else {
return Err(PatchExecutorError::BackupNotEnabled);
}
Ok(())
}
pub fn work_dir(&self) -> &Path {
&self.work_dir
}
pub fn is_backup_enabled(&self) -> bool {
self.backup_dir.is_some()
}
pub fn patch_source(&self) -> Option<&Path> {
self.patch_source.as_deref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use tokio::fs;
#[tokio::test]
async fn test_file_operation_executor_creation() {
let temp_dir = TempDir::new().unwrap();
let executor = FileOperationExecutor::new(temp_dir.path().to_owned());
assert!(executor.is_ok());
}
#[tokio::test]
async fn test_enable_backup() {
let temp_dir = TempDir::new().unwrap();
let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
assert!(!executor.is_backup_enabled());
let result = executor.enable_backup();
assert!(result.is_ok());
assert!(executor.is_backup_enabled());
}
#[tokio::test]
async fn test_invalid_work_dir() {
let invalid_path = PathBuf::from("/nonexistent/path");
let executor = FileOperationExecutor::new(invalid_path);
assert!(executor.is_err());
}
#[tokio::test]
async fn test_set_patch_source() {
let temp_dir = TempDir::new().unwrap();
let patch_source_dir = TempDir::new().unwrap();
let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
let result = executor.set_patch_source(patch_source_dir.path());
assert!(result.is_ok());
assert_eq!(executor.patch_source(), Some(patch_source_dir.path()));
}
#[tokio::test]
async fn test_atomic_file_replace() {
let temp_dir = TempDir::new().unwrap();
let executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
let source_file = temp_dir.path().join("source.txt");
let content = "test content";
fs::write(&source_file, content).await.unwrap();
let target_file = temp_dir.path().join("target.txt");
executor
.atomic_file_replace(&source_file, &target_file)
.await
.unwrap();
let target_content = fs::read_to_string(&target_file).await.unwrap();
assert_eq!(target_content, content);
}
#[tokio::test]
async fn test_file_replacement_with_backup() {
let temp_dir = TempDir::new().unwrap();
let patch_source_dir = TempDir::new().unwrap();
let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
executor.enable_backup().unwrap();
executor.set_patch_source(patch_source_dir.path()).unwrap();
let original_file = temp_dir.path().join("test.txt");
let original_content = "original content";
fs::write(&original_file, original_content).await.unwrap();
let patch_file = patch_source_dir.path().join("test.txt");
let patch_content = "new content";
fs::write(&patch_file, patch_content).await.unwrap();
executor
.replace_files(&["test.txt".to_string()])
.await
.unwrap();
let new_content = fs::read_to_string(&original_file).await.unwrap();
assert_eq!(new_content, patch_content);
executor.rollback().await.unwrap();
let restored_content = fs::read_to_string(&original_file).await.unwrap();
assert_eq!(restored_content, original_content);
}
#[tokio::test]
async fn test_directory_operations() {
let temp_dir = TempDir::new().unwrap();
let patch_source_dir = TempDir::new().unwrap();
let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
executor.enable_backup().unwrap();
executor.set_patch_source(patch_source_dir.path()).unwrap();
let original_dir = temp_dir.path().join("testdir");
fs::create_dir_all(&original_dir).await.unwrap();
fs::write(original_dir.join("file1.txt"), "original file1")
.await
.unwrap();
let patch_dir = patch_source_dir.path().join("testdir");
fs::create_dir_all(&patch_dir).await.unwrap();
fs::write(patch_dir.join("file2.txt"), "new file2")
.await
.unwrap();
executor
.replace_directories(&["testdir".to_string()])
.await
.unwrap();
assert!(!original_dir.join("file1.txt").exists());
assert!(original_dir.join("file2.txt").exists());
let new_content = fs::read_to_string(original_dir.join("file2.txt"))
.await
.unwrap();
assert_eq!(new_content, "new file2");
}
#[tokio::test]
async fn test_delete_operations() {
let temp_dir = TempDir::new().unwrap();
let mut executor = FileOperationExecutor::new(temp_dir.path().to_owned()).unwrap();
executor.enable_backup().unwrap();
let test_file = temp_dir.path().join("to_delete.txt");
fs::write(&test_file, "delete me").await.unwrap();
executor
.delete_items(&["to_delete.txt".to_string()])
.await
.unwrap();
assert!(!test_file.exists());
executor.rollback().await.unwrap();
assert!(test_file.exists());
let restored_content = fs::read_to_string(&test_file).await.unwrap();
assert_eq!(restored_content, "delete me");
}
}