- Add new documentation sections for PostgreSQL installer functions and usage examples. Improves clarity and completeness of the documentation. - Add new files and patterns to .gitignore to prevent unnecessary files from being committed to the repository. Improves repository cleanliness and reduces clutter.
1095 lines
32 KiB
Rust
1095 lines
32 KiB
Rust
use std::error::Error;
|
|
use std::fmt;
|
|
use std::fs;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
// 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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::copy;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* // 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")?;
|
|
*
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```
|
|
* use sal::os::exist;
|
|
*
|
|
* 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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::find_file;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let file_path = find_file("/path/to/dir", "*.txt")?;
|
|
* println!("Found file: {}", file_path);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::find_files;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let files = find_files("/path/to/dir", "*.txt")?;
|
|
* for file in files {
|
|
* println!("Found file: {}", file);
|
|
* }
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::find_dir;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let dir_path = find_dir("/path/to/parent", "sub*")?;
|
|
* println!("Found directory: {}", dir_path);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::find_dirs;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let dirs = find_dirs("/path/to/parent", "sub*")?;
|
|
* for dir in dirs {
|
|
* println!("Found directory: {}", dir);
|
|
* }
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```
|
|
* use sal::os::delete;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* // Delete a file
|
|
* let result = delete("file.txt")?;
|
|
*
|
|
* // Delete a directory and all its contents
|
|
* let result = delete("directory/")?;
|
|
*
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```
|
|
* use sal::os::mkdir;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let result = mkdir("path/to/new/directory")?;
|
|
* println!("{}", result);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::file_size;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let size = file_size("file.txt")?;
|
|
* println!("File size: {} bytes", size);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::rsync;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let result = rsync("source_dir/", "backup_dir/")?;
|
|
* println!("{}", result);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::chdir;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let result = chdir("/path/to/directory")?;
|
|
* println!("{}", result);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::file_read;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let content = file_read("file.txt")?;
|
|
* println!("File content: {}", content);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```
|
|
* use sal::os::file_write;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let result = file_write("file.txt", "Hello, world!")?;
|
|
* println!("{}", result);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```
|
|
* use sal::os::file_write_append;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* let result = file_write_append("log.txt", "New log entry\n")?;
|
|
* println!("{}", result);
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```no_run
|
|
* use sal::os::mv;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* // 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")?;
|
|
*
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
*
|
|
* ```
|
|
* use sal::os::which;
|
|
*
|
|
* 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
|
|
*
|
|
* ```
|
|
* use sal::os::cmd_ensure_exists;
|
|
*
|
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
* // 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")?;
|
|
*
|
|
* Ok(())
|
|
* }
|
|
* ```
|
|
*/
|
|
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(", ")))
|
|
}
|
|
}
|