use std::fs; use std::path::Path; use std::process::Command; use std::fmt; use std::error::Error; use std::io; // Define a custom error type for file system operations #[derive(Debug)] pub enum FsError { DirectoryNotFound(String), FileNotFound(String), CreateDirectoryFailed(io::Error), CopyFailed(io::Error), DeleteFailed(io::Error), CommandFailed(String), CommandExecutionError(io::Error), InvalidGlobPattern(glob::PatternError), NotADirectory(String), NotAFile(String), UnknownFileType(String), MetadataError(io::Error), ChangeDirFailed(io::Error), } // Implement Display for FsError impl fmt::Display for FsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir), FsError::FileNotFound(pattern) => write!(f, "No files found matching '{}'", pattern), FsError::CreateDirectoryFailed(e) => write!(f, "Failed to create parent directories: {}", e), FsError::CopyFailed(e) => write!(f, "Failed to copy file: {}", e), FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e), FsError::CommandFailed(e) => write!(f, "{}", e), FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e), FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e), FsError::NotADirectory(path) => write!(f, "Path '{}' exists but is not a directory", path), FsError::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path), FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path), FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e), FsError::ChangeDirFailed(e) => write!(f, "Failed to change directory: {}", e), } } } // Implement Error trait for FsError impl Error for FsError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { FsError::CreateDirectoryFailed(e) => Some(e), FsError::CopyFailed(e) => Some(e), FsError::DeleteFailed(e) => Some(e), FsError::CommandExecutionError(e) => Some(e), FsError::InvalidGlobPattern(e) => Some(e), FsError::MetadataError(e) => Some(e), FsError::ChangeDirFailed(e) => Some(e), _ => None, } } } /** * Recursively copy a file or directory from source to destination. * * # Arguments * * * `src` - The source path, which can include wildcards * * `dest` - The destination path * * # Returns * * * `Ok(String)` - A success message indicating what was copied * * `Err(FsError)` - An error if the copy operation failed * * # Examples * * ``` * // Copy a single file * let result = copy("file.txt", "backup/file.txt")?; * * // Copy multiple files using wildcards * let result = copy("*.txt", "backup/")?; * * // Copy a directory recursively * let result = copy("src_dir", "dest_dir")?; * ``` */ pub fn copy(src: &str, dest: &str) -> Result { let dest_path = Path::new(dest); // Check if source path contains wildcards if src.contains('*') || src.contains('?') || src.contains('[') { // Create parent directories for destination if needed if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Use glob to expand wildcards let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?; let paths: Vec<_> = entries .filter_map(Result::ok) .collect(); if paths.is_empty() { return Err(FsError::FileNotFound(src.to_string())); } let mut success_count = 0; let dest_is_dir = dest_path.exists() && dest_path.is_dir(); for path in paths { let target_path = if dest_is_dir { // If destination is a directory, copy the file into it dest_path.join(path.file_name().unwrap_or_default()) } else { // Otherwise use the destination as is (only makes sense for single file) dest_path.to_path_buf() }; if path.is_file() { // Copy file if let Err(e) = fs::copy(&path, &target_path) { println!("Warning: Failed to copy {}: {}", path.display(), e); } else { success_count += 1; } } else if path.is_dir() { // For directories, use platform-specific command #[cfg(target_os = "windows")] let output = Command::new("xcopy") .args(&["/E", "/I", "/H", "/Y", &path.to_string_lossy(), &target_path.to_string_lossy()]) .status(); #[cfg(not(target_os = "windows"))] let output = Command::new("cp") .args(&["-R", &path.to_string_lossy(), &target_path.to_string_lossy()]) .status(); match output { Ok(status) => { if status.success() { success_count += 1; } }, Err(e) => println!("Warning: Failed to copy directory {}: {}", path.display(), e), } } } if success_count > 0 { Ok(format!("Successfully copied {} items from '{}' to '{}'", success_count, src, dest)) } else { Err(FsError::CommandFailed(format!("Failed to copy any files from '{}' to '{}'", src, dest))) } } else { // Handle non-wildcard paths normally let src_path = Path::new(src); // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Copy based on source type if src_path.is_file() { // Copy file fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?; Ok(format!("Successfully copied file '{}' to '{}'", src, dest)) } else if src_path.is_dir() { // For directories, use platform-specific command #[cfg(target_os = "windows")] let output = Command::new("xcopy") .args(&["/E", "/I", "/H", "/Y", src, dest]) .output(); #[cfg(not(target_os = "windows"))] let output = Command::new("cp") .args(&["-R", src, dest]) .output(); match output { Ok(out) => { if out.status.success() { Ok(format!("Successfully copied directory '{}' to '{}'", src, dest)) } else { let error = String::from_utf8_lossy(&out.stderr); Err(FsError::CommandFailed(format!("Failed to copy directory: {}", error))) } }, Err(e) => Err(FsError::CommandExecutionError(e)), } } else { Err(FsError::UnknownFileType(src.to_string())) } } } /** * Check if a file or directory exists. * * # Arguments * * * `path` - The path to check * * # Returns * * * `bool` - True if the path exists, false otherwise * * # Examples * * ``` * if exist("file.txt") { * println!("File exists"); * } * ``` */ pub fn exist(path: &str) -> bool { Path::new(path).exists() } /** * Find a file in a directory (with support for wildcards). * * # Arguments * * * `dir` - The directory to search in * * `filename` - The filename pattern to search for (can include wildcards) * * # Returns * * * `Ok(String)` - The path to the found file * * `Err(FsError)` - An error if no file is found or multiple files are found * * # Examples * * ``` * let file_path = find_file("/path/to/dir", "*.txt")?; * println!("Found file: {}", file_path); * ``` */ pub fn find_file(dir: &str, filename: &str) -> Result { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find files - use recursive pattern to find in subdirectories too let pattern = format!("{}/**/{}", dir, filename); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let files: Vec<_> = entries .filter_map(Result::ok) .filter(|path| path.is_file()) .collect(); match files.len() { 0 => Err(FsError::FileNotFound(filename.to_string())), 1 => Ok(files[0].to_string_lossy().to_string()), _ => { // If multiple matches, just return the first one instead of erroring // This makes wildcard searches more practical println!("Note: Multiple files found matching '{}', returning first match", filename); Ok(files[0].to_string_lossy().to_string()) } } } /** * Find multiple files in a directory (recursive, with support for wildcards). * * # Arguments * * * `dir` - The directory to search in * * `filename` - The filename pattern to search for (can include wildcards) * * # Returns * * * `Ok(Vec)` - A vector of paths to the found files * * `Err(FsError)` - An error if the directory doesn't exist or the pattern is invalid * * # Examples * * ``` * let files = find_files("/path/to/dir", "*.txt")?; * for file in files { * println!("Found file: {}", file); * } * ``` */ pub fn find_files(dir: &str, filename: &str) -> Result, FsError> { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find files let pattern = format!("{}/**/{}", dir, filename); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let files: Vec = entries .filter_map(Result::ok) .filter(|path| path.is_file()) .map(|path| path.to_string_lossy().to_string()) .collect(); Ok(files) } /** * Find a directory in a parent directory (with support for wildcards). * * # Arguments * * * `dir` - The parent directory to search in * * `dirname` - The directory name pattern to search for (can include wildcards) * * # Returns * * * `Ok(String)` - The path to the found directory * * `Err(FsError)` - An error if no directory is found or multiple directories are found * * # Examples * * ``` * let dir_path = find_dir("/path/to/parent", "sub*")?; * println!("Found directory: {}", dir_path); * ``` */ pub fn find_dir(dir: &str, dirname: &str) -> Result { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find directories let pattern = format!("{}/{}", dir, dirname); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let dirs: Vec<_> = entries .filter_map(Result::ok) .filter(|path| path.is_dir()) .collect(); match dirs.len() { 0 => Err(FsError::DirectoryNotFound(dirname.to_string())), 1 => Ok(dirs[0].to_string_lossy().to_string()), _ => Err(FsError::CommandFailed(format!("Multiple directories found matching '{}', expected only one", dirname))), } } /** * Find multiple directories in a parent directory (recursive, with support for wildcards). * * # Arguments * * * `dir` - The parent directory to search in * * `dirname` - The directory name pattern to search for (can include wildcards) * * # Returns * * * `Ok(Vec)` - A vector of paths to the found directories * * `Err(FsError)` - An error if the parent directory doesn't exist or the pattern is invalid * * # Examples * * ``` * let dirs = find_dirs("/path/to/parent", "sub*")?; * for dir in dirs { * println!("Found directory: {}", dir); * } * ``` */ pub fn find_dirs(dir: &str, dirname: &str) -> Result, FsError> { let dir_path = Path::new(dir); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } // Use glob to find directories let pattern = format!("{}/**/{}", dir, dirname); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; let dirs: Vec = entries .filter_map(Result::ok) .filter(|path| path.is_dir()) .map(|path| path.to_string_lossy().to_string()) .collect(); Ok(dirs) } /** * Delete a file or directory (defensive - doesn't error if file doesn't exist). * * # Arguments * * * `path` - The path to delete * * # Returns * * * `Ok(String)` - A success message indicating what was deleted * * `Err(FsError)` - An error if the deletion failed * * # Examples * * ``` * // Delete a file * let result = delete("file.txt")?; * * // Delete a directory and all its contents * let result = delete("directory/")?; * ``` */ pub fn delete(path: &str) -> Result { let path_obj = Path::new(path); // Check if path exists if !path_obj.exists() { return Ok(format!("Nothing to delete at '{}'", path)); } // Delete based on path type if path_obj.is_file() || path_obj.is_symlink() { fs::remove_file(path_obj).map_err(FsError::DeleteFailed)?; Ok(format!("Successfully deleted file '{}'", path)) } else if path_obj.is_dir() { fs::remove_dir_all(path_obj).map_err(FsError::DeleteFailed)?; Ok(format!("Successfully deleted directory '{}'", path)) } else { Err(FsError::UnknownFileType(path.to_string())) } } /** * Create a directory and all parent directories (defensive - doesn't error if directory exists). * * # Arguments * * * `path` - The path of the directory to create * * # Returns * * * `Ok(String)` - A success message indicating the directory was created * * `Err(FsError)` - An error if the creation failed * * # Examples * * ``` * let result = mkdir("path/to/new/directory")?; * println!("{}", result); * ``` */ pub fn mkdir(path: &str) -> Result { let path_obj = Path::new(path); // Check if path already exists if path_obj.exists() { if path_obj.is_dir() { return Ok(format!("Directory '{}' already exists", path)); } else { return Err(FsError::NotADirectory(path.to_string())); } } // Create directory and parents fs::create_dir_all(path_obj).map_err(FsError::CreateDirectoryFailed)?; Ok(format!("Successfully created directory '{}'", path)) } /** * Get the size of a file in bytes. * * # Arguments * * * `path` - The path of the file * * # Returns * * * `Ok(i64)` - The size of the file in bytes * * `Err(FsError)` - An error if the file doesn't exist or isn't a regular file * * # Examples * * ``` * let size = file_size("file.txt")?; * println!("File size: {} bytes", size); * ``` */ pub fn file_size(path: &str) -> Result { let path_obj = Path::new(path); // Check if file exists if !path_obj.exists() { return Err(FsError::FileNotFound(path.to_string())); } // Check if it's a regular file if !path_obj.is_file() { return Err(FsError::NotAFile(path.to_string())); } // Get file metadata let metadata = fs::metadata(path_obj).map_err(FsError::MetadataError)?; Ok(metadata.len() as i64) } /** * Sync directories using rsync (or platform equivalent). * * # Arguments * * * `src` - The source directory * * `dest` - The destination directory * * # Returns * * * `Ok(String)` - A success message indicating the directories were synced * * `Err(FsError)` - An error if the sync failed * * # Examples * * ``` * let result = rsync("source_dir/", "backup_dir/")?; * println!("{}", result); * ``` */ pub fn rsync(src: &str, dest: &str) -> Result { let src_path = Path::new(src); let dest_path = Path::new(dest); // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } // Use platform-specific command for syncing #[cfg(target_os = "windows")] let output = Command::new("robocopy") .args(&[src, dest, "/MIR", "/NFL", "/NDL"]) .output(); #[cfg(any(target_os = "macos", target_os = "linux"))] let output = Command::new("rsync") .args(&["-a", "--delete", src, dest]) .output(); match output { Ok(out) => { if out.status.success() || out.status.code() == Some(1) { // rsync and robocopy return 1 for some non-error cases Ok(format!("Successfully synced '{}' to '{}'", src, dest)) } else { let error = String::from_utf8_lossy(&out.stderr); Err(FsError::CommandFailed(format!("Failed to sync directories: {}", error))) } }, Err(e) => Err(FsError::CommandExecutionError(e)), } } /** * Change the current working directory. * * # Arguments * * * `path` - The path to change to * * # Returns * * * `Ok(String)` - A success message indicating the directory was changed * * `Err(FsError)` - An error if the directory change failed * * # Examples * * ``` * let result = chdir("/path/to/directory")?; * println!("{}", result); * ``` */ pub fn chdir(path: &str) -> Result { let path_obj = Path::new(path); // Check if directory exists if !path_obj.exists() { return Err(FsError::DirectoryNotFound(path.to_string())); } // Check if it's a directory if !path_obj.is_dir() { return Err(FsError::NotADirectory(path.to_string())); } // Change directory std::env::set_current_dir(path_obj).map_err(FsError::ChangeDirFailed)?; Ok(format!("Successfully changed directory to '{}'", path)) }