972 lines
30 KiB
Rust
972 lines
30 KiB
Rust
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),
|
|
CommandNotFound(String),
|
|
CommandExecutionError(io::Error),
|
|
InvalidGlobPattern(glob::PatternError),
|
|
NotADirectory(String),
|
|
NotAFile(String),
|
|
UnknownFileType(String),
|
|
MetadataError(io::Error),
|
|
ChangeDirFailed(io::Error),
|
|
ReadFailed(io::Error),
|
|
WriteFailed(io::Error),
|
|
AppendFailed(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::CommandNotFound(e) => write!(f, "Command not found: {}", 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),
|
|
FsError::ReadFailed(e) => write!(f, "Failed to read file: {}", e),
|
|
FsError::WriteFailed(e) => write!(f, "Failed to write to file: {}", e),
|
|
FsError::AppendFailed(e) => write!(f, "Failed to append to file: {}", 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),
|
|
FsError::ReadFailed(e) => Some(e),
|
|
FsError::WriteFailed(e) => Some(e),
|
|
FsError::AppendFailed(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<String, FsError> {
|
|
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
|
|
if path.is_file() {
|
|
// For files, just use the filename
|
|
dest_path.join(path.file_name().unwrap_or_default())
|
|
} else if path.is_dir() {
|
|
// For directories, use the directory name
|
|
dest_path.join(path.file_name().unwrap_or_default())
|
|
} else {
|
|
// Fallback
|
|
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() {
|
|
// If destination is a directory, copy the file into it
|
|
if dest_path.exists() && dest_path.is_dir() {
|
|
let file_name = src_path.file_name().unwrap_or_default();
|
|
let new_dest_path = dest_path.join(file_name);
|
|
fs::copy(src_path, new_dest_path).map_err(FsError::CopyFailed)?;
|
|
Ok(format!("Successfully copied file '{}' to '{}/{}'", src, dest, file_name.to_string_lossy()))
|
|
} else {
|
|
// Otherwise copy file to the specified destination
|
|
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<String, 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 - 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<String>)` - 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<Vec<String>, 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<String> = 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<String, 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())
|
|
.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<String>)` - 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<Vec<String>, 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<String> = 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<String, FsError> {
|
|
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<String, FsError> {
|
|
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<i64, FsError> {
|
|
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<String, FsError> {
|
|
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<String, FsError> {
|
|
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))
|
|
}
|
|
|
|
/**
|
|
* Read the contents of a file.
|
|
*
|
|
* # Arguments
|
|
*
|
|
* * `path` - The path of the file to read
|
|
*
|
|
* # Returns
|
|
*
|
|
* * `Ok(String)` - The contents of the file
|
|
* * `Err(FsError)` - An error if the file doesn't exist or can't be read
|
|
*
|
|
* # Examples
|
|
*
|
|
* ```
|
|
* let content = file_read("file.txt")?;
|
|
* println!("File content: {}", content);
|
|
* ```
|
|
*/
|
|
pub fn file_read(path: &str) -> Result<String, FsError> {
|
|
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()));
|
|
}
|
|
|
|
// Read file content
|
|
fs::read_to_string(path_obj).map_err(FsError::ReadFailed)
|
|
}
|
|
|
|
/**
|
|
* Write content to a file (creates the file if it doesn't exist, overwrites if it does).
|
|
*
|
|
* # Arguments
|
|
*
|
|
* * `path` - The path of the file to write to
|
|
* * `content` - The content to write to the file
|
|
*
|
|
* # Returns
|
|
*
|
|
* * `Ok(String)` - A success message indicating the file was written
|
|
* * `Err(FsError)` - An error if the file can't be written
|
|
*
|
|
* # Examples
|
|
*
|
|
* ```
|
|
* let result = file_write("file.txt", "Hello, world!")?;
|
|
* println!("{}", result);
|
|
* ```
|
|
*/
|
|
pub fn file_write(path: &str, content: &str) -> Result<String, FsError> {
|
|
let path_obj = Path::new(path);
|
|
|
|
// Create parent directories if they don't exist
|
|
if let Some(parent) = path_obj.parent() {
|
|
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
|
|
}
|
|
|
|
// Write content to file
|
|
fs::write(path_obj, content).map_err(FsError::WriteFailed)?;
|
|
|
|
Ok(format!("Successfully wrote to file '{}'", path))
|
|
}
|
|
|
|
/**
|
|
* Append content to a file (creates the file if it doesn't exist).
|
|
*
|
|
* # Arguments
|
|
*
|
|
* * `path` - The path of the file to append to
|
|
* * `content` - The content to append to the file
|
|
*
|
|
* # Returns
|
|
*
|
|
* * `Ok(String)` - A success message indicating the content was appended
|
|
* * `Err(FsError)` - An error if the file can't be appended to
|
|
*
|
|
* # Examples
|
|
*
|
|
* ```
|
|
* let result = file_write_append("log.txt", "New log entry\n")?;
|
|
* println!("{}", result);
|
|
* ```
|
|
*/
|
|
pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
|
|
let path_obj = Path::new(path);
|
|
|
|
// Create parent directories if they don't exist
|
|
if let Some(parent) = path_obj.parent() {
|
|
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
|
|
}
|
|
|
|
// Open file in append mode (or create if it doesn't exist)
|
|
let mut file = fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(path_obj)
|
|
.map_err(FsError::AppendFailed)?;
|
|
|
|
// Append content to file
|
|
use std::io::Write;
|
|
file.write_all(content.as_bytes()).map_err(FsError::AppendFailed)?;
|
|
|
|
Ok(format!("Successfully appended to file '{}'", path))
|
|
}
|
|
|
|
/**
|
|
* Move a file or directory from source to destination.
|
|
*
|
|
* # Arguments
|
|
*
|
|
* * `src` - The source path
|
|
* * `dest` - The destination path
|
|
*
|
|
* # Returns
|
|
*
|
|
* * `Ok(String)` - A success message indicating what was moved
|
|
* * `Err(FsError)` - An error if the move operation failed
|
|
*
|
|
* # Examples
|
|
*
|
|
* ```
|
|
* // Move a file
|
|
* let result = mv("file.txt", "new_location/file.txt")?;
|
|
*
|
|
* // Move a directory
|
|
* let result = mv("src_dir", "dest_dir")?;
|
|
*
|
|
* // Rename a file
|
|
* let result = mv("old_name.txt", "new_name.txt")?;
|
|
* ```
|
|
*/
|
|
pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
|
|
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)?;
|
|
}
|
|
|
|
// Handle the case where destination is a directory and exists
|
|
let final_dest_path = if dest_path.exists() && dest_path.is_dir() && src_path.is_file() {
|
|
// If destination is a directory and source is a file, move the file into the directory
|
|
let file_name = src_path.file_name().unwrap_or_default();
|
|
dest_path.join(file_name)
|
|
} else {
|
|
dest_path.to_path_buf()
|
|
};
|
|
|
|
// Clone the path for use in the error handler
|
|
let final_dest_path_clone = final_dest_path.clone();
|
|
|
|
// Perform the move operation
|
|
fs::rename(src_path, &final_dest_path).map_err(|e| {
|
|
// If rename fails (possibly due to cross-device link), try copy and delete
|
|
if e.kind() == std::io::ErrorKind::CrossesDevices {
|
|
// For cross-device moves, we need to copy and then delete
|
|
if src_path.is_file() {
|
|
// Copy file
|
|
match fs::copy(src_path, &final_dest_path_clone) {
|
|
Ok(_) => {
|
|
// Delete source after successful copy
|
|
if let Err(del_err) = fs::remove_file(src_path) {
|
|
return FsError::DeleteFailed(del_err);
|
|
}
|
|
return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
|
|
},
|
|
Err(copy_err) => return FsError::CopyFailed(copy_err),
|
|
}
|
|
} 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])
|
|
.status();
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
let output = Command::new("cp")
|
|
.args(&["-R", src, dest])
|
|
.status();
|
|
|
|
match output {
|
|
Ok(status) => {
|
|
if status.success() {
|
|
// Delete source after successful copy
|
|
if let Err(del_err) = fs::remove_dir_all(src_path) {
|
|
return FsError::DeleteFailed(del_err);
|
|
}
|
|
return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
|
|
} else {
|
|
return FsError::CommandFailed("Failed to copy directory for move operation".to_string());
|
|
}
|
|
},
|
|
Err(cmd_err) => return FsError::CommandExecutionError(cmd_err),
|
|
}
|
|
}
|
|
}
|
|
FsError::CommandFailed(format!("Failed to move '{}' to '{}': {}", src, dest, e))
|
|
})?;
|
|
|
|
// If we get here, either the rename was successful or our copy-delete hack worked
|
|
if src_path.is_file() {
|
|
Ok(format!("Successfully moved file '{}' to '{}'", src, dest))
|
|
} else {
|
|
Ok(format!("Successfully moved directory '{}' to '{}'", src, dest))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a command exists in the system PATH.
|
|
*
|
|
* # Arguments
|
|
*
|
|
* * `command` - The command to check
|
|
*
|
|
* # Returns
|
|
*
|
|
* * `String` - Empty string if the command doesn't exist, path to the command if it does
|
|
*
|
|
* # Examples
|
|
*
|
|
* ```
|
|
* let cmd_path = which("ls");
|
|
* if cmd_path != "" {
|
|
* println!("ls is available at: {}", cmd_path);
|
|
* }
|
|
* ```
|
|
*/
|
|
pub fn which(command: &str) -> String {
|
|
// Use the appropriate command based on the platform
|
|
#[cfg(target_os = "windows")]
|
|
let output = Command::new("where")
|
|
.arg(command)
|
|
.output();
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
let output = Command::new("which")
|
|
.arg(command)
|
|
.output();
|
|
|
|
match output {
|
|
Ok(out) => {
|
|
if out.status.success() {
|
|
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
path
|
|
} else {
|
|
String::new()
|
|
}
|
|
},
|
|
Err(_) => String::new(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure that one or more commands exist in the system PATH.
|
|
* If any command doesn't exist, an error is thrown.
|
|
*
|
|
* # Arguments
|
|
*
|
|
* * `commands` - The command(s) to check, comma-separated for multiple commands
|
|
*
|
|
* # Returns
|
|
*
|
|
* * `Ok(String)` - A success message indicating all commands exist
|
|
* * `Err(FsError)` - An error if any command doesn't exist
|
|
*
|
|
* # Examples
|
|
*
|
|
* ```
|
|
* // Check if a single command exists
|
|
* let result = cmd_ensure_exists("nerdctl")?;
|
|
*
|
|
* // Check if multiple commands exist
|
|
* let result = cmd_ensure_exists("nerdctl,docker,containerd")?;
|
|
* ```
|
|
*/
|
|
pub fn cmd_ensure_exists(commands: &str) -> Result<String, FsError> {
|
|
// Split the input by commas to handle multiple commands
|
|
let command_list: Vec<&str> = commands.split(',')
|
|
.map(|s| s.trim())
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
|
|
if command_list.is_empty() {
|
|
return Err(FsError::CommandFailed("No commands specified to check".to_string()));
|
|
}
|
|
|
|
let mut missing_commands = Vec::new();
|
|
|
|
// Check each command
|
|
for cmd in &command_list {
|
|
let cmd_path = which(cmd);
|
|
if cmd_path.is_empty() {
|
|
missing_commands.push(cmd.to_string());
|
|
}
|
|
}
|
|
|
|
// If any commands are missing, return an error
|
|
if !missing_commands.is_empty() {
|
|
return Err(FsError::CommandNotFound(missing_commands.join(", ")));
|
|
}
|
|
|
|
// All commands exist
|
|
if command_list.len() == 1 {
|
|
Ok(format!("Command '{}' exists", command_list[0]))
|
|
} else {
|
|
Ok(format!("All commands exist: {}", command_list.join(", ")))
|
|
}
|
|
}
|