use dirs;
use libc;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::Command;
#[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),
}
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),
}
}
}
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,
}
}
}
#[cfg(not(target_os = "windows"))]
fn set_executable(path: &Path) -> Result<(), io::Error> {
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o755); fs::set_permissions(path, perms)
}
fn copy_internal(src: &str, dest: &str, make_executable: bool) -> Result<String, FsError> {
let dest_path = Path::new(dest);
if src.contains('*') || src.contains('?') || src.contains('[') {
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
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 path.is_file() {
dest_path.join(path.file_name().unwrap_or_default())
} else if path.is_dir() {
dest_path.join(path.file_name().unwrap_or_default())
} else {
dest_path.join(path.file_name().unwrap_or_default())
}
} else {
dest_path.to_path_buf()
};
if path.is_file() {
if let Err(e) = fs::copy(&path, &target_path) {
println!("Warning: Failed to copy {}: {}", path.display(), e);
} else {
success_count += 1;
if make_executable {
#[cfg(not(target_os = "windows"))]
{
if let Err(e) = set_executable(&target_path) {
println!(
"Warning: Failed to make {} executable: {}",
target_path.display(),
e
);
}
}
}
}
} else if path.is_dir() {
#[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 {
let src_path = Path::new(src);
if !src_path.exists() {
return Err(FsError::FileNotFound(src.to_string()));
}
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
if src_path.is_file() {
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)?;
if make_executable {
#[cfg(not(target_os = "windows"))]
{
if let Err(e) = set_executable(&new_dest_path) {
println!(
"Warning: Failed to make {} executable: {}",
new_dest_path.display(),
e
);
}
}
}
Ok(format!(
"Successfully copied file '{}' to '{}/{}'",
src,
dest,
file_name.to_string_lossy()
))
} else {
fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?;
if make_executable {
#[cfg(not(target_os = "windows"))]
{
if let Err(e) = set_executable(dest_path) {
println!(
"Warning: Failed to make {} executable: {}",
dest_path.display(),
e
);
}
}
}
Ok(format!("Successfully copied file '{}' to '{}'", src, dest))
}
} else if src_path.is_dir() {
#[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()))
}
}
}
pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
copy_internal(src, dest, false)
}
pub fn copy_bin(src: &str) -> Result<String, FsError> {
let dest_path = if cfg!(target_os = "linux") && unsafe { libc::getuid() } == 0 {
Path::new("/usr/local/bin").to_path_buf()
} else {
dirs::home_dir()
.ok_or_else(|| FsError::DirectoryNotFound("Home directory not found".to_string()))?
.join("hero/bin")
};
let result = copy_internal(src, dest_path.to_str().unwrap(), true);
if let Ok(msg) = &result {
println!("{}", msg);
}
result
}
pub fn exist(path: &str) -> bool {
Path::new(path).exists()
}
pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> {
let dir_path = Path::new(dir);
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
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()),
_ => {
println!(
"Note: Multiple files found matching '{}', returning first match",
filename
);
Ok(files[0].to_string_lossy().to_string())
}
}
}
pub fn find_files(dir: &str, filename: &str) -> Result<Vec<String>, FsError> {
let dir_path = Path::new(dir);
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
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)
}
pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> {
let dir_path = Path::new(dir);
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
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
))),
}
}
pub fn find_dirs(dir: &str, dirname: &str) -> Result<Vec<String>, FsError> {
let dir_path = Path::new(dir);
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
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)
}
pub fn delete(path: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Ok(format!("Nothing to delete at '{}'", path));
}
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()))
}
}
pub fn mkdir(path: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
if path_obj.exists() {
if path_obj.is_dir() {
return Ok(format!("Directory '{}' already exists", path));
} else {
return Err(FsError::NotADirectory(path.to_string()));
}
}
fs::create_dir_all(path_obj).map_err(FsError::CreateDirectoryFailed)?;
Ok(format!("Successfully created directory '{}'", path))
}
pub fn file_size(path: &str) -> Result<i64, FsError> {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Err(FsError::FileNotFound(path.to_string()));
}
if !path_obj.is_file() {
return Err(FsError::NotAFile(path.to_string()));
}
let metadata = fs::metadata(path_obj).map_err(FsError::MetadataError)?;
Ok(metadata.len() as i64)
}
pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> {
let src_path = Path::new(src);
let dest_path = Path::new(dest);
if !src_path.exists() {
return Err(FsError::FileNotFound(src.to_string()));
}
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
#[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) {
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)),
}
}
pub fn chdir(path: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Err(FsError::DirectoryNotFound(path.to_string()));
}
if !path_obj.is_dir() {
return Err(FsError::NotADirectory(path.to_string()));
}
std::env::set_current_dir(path_obj).map_err(FsError::ChangeDirFailed)?;
Ok(format!("Successfully changed directory to '{}'", path))
}
pub fn file_read(path: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Err(FsError::FileNotFound(path.to_string()));
}
if !path_obj.is_file() {
return Err(FsError::NotAFile(path.to_string()));
}
fs::read_to_string(path_obj).map_err(FsError::ReadFailed)
}
pub fn file_write(path: &str, content: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
if let Some(parent) = path_obj.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
fs::write(path_obj, content).map_err(FsError::WriteFailed)?;
Ok(format!("Successfully wrote to file '{}'", path))
}
pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
if let Some(parent) = path_obj.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(path_obj)
.map_err(FsError::AppendFailed)?;
use std::io::Write;
file.write_all(content.as_bytes())
.map_err(FsError::AppendFailed)?;
Ok(format!("Successfully appended to file '{}'", path))
}
pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
let src_path = Path::new(src);
let dest_path = Path::new(dest);
if !src_path.exists() {
return Err(FsError::FileNotFound(src.to_string()));
}
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
let final_dest_path = if dest_path.exists() && dest_path.is_dir() && src_path.is_file() {
let file_name = src_path.file_name().unwrap_or_default();
dest_path.join(file_name)
} else {
dest_path.to_path_buf()
};
let final_dest_path_clone = final_dest_path.clone();
fs::rename(src_path, &final_dest_path).map_err(|e| {
if e.kind() == std::io::ErrorKind::CrossesDevices {
if src_path.is_file() {
match fs::copy(src_path, &final_dest_path_clone) {
Ok(_) => {
if let Err(del_err) = fs::remove_file(src_path) {
return FsError::DeleteFailed(del_err);
}
return FsError::CommandFailed("".to_string()); }
Err(copy_err) => return FsError::CopyFailed(copy_err),
}
} else if src_path.is_dir() {
#[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() {
if let Err(del_err) = fs::remove_dir_all(src_path) {
return FsError::DeleteFailed(del_err);
}
return FsError::CommandFailed("".to_string()); } 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 src_path.is_file() {
Ok(format!("Successfully moved file '{}' to '{}'", src, dest))
} else {
Ok(format!(
"Successfully moved directory '{}' to '{}'",
src, dest
))
}
}
pub fn which(command: &str) -> String {
#[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(),
}
}
pub fn cmd_ensure_exists(commands: &str) -> Result<String, FsError> {
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();
for cmd in &command_list {
let cmd_path = which(cmd);
if cmd_path.is_empty() {
missing_commands.push(cmd.to_string());
}
}
if !missing_commands.is_empty() {
return Err(FsError::CommandNotFound(missing_commands.join(", ")));
}
if command_list.len() == 1 {
Ok(format!("Command '{}' exists", command_list[0]))
} else {
Ok(format!("All commands exist: {}", command_list.join(", ")))
}
}