...
This commit is contained in:
parent
d8de0d7ebf
commit
5678a9aa35
5
.gitignore
vendored
5
.gitignore
vendored
@ -14,3 +14,8 @@ Cargo.lock
|
|||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
|
||||||
|
/target
|
||||||
|
28
Cargo.toml
Normal file
28
Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "sal"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Your Name <your.email@example.com>"]
|
||||||
|
description = "System Abstraction Layer - A library for easy interaction with operating system features"
|
||||||
|
repository = "https://github.com/yourusername/sal"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
keywords = ["system", "os", "abstraction", "platform", "filesystem"]
|
||||||
|
categories = ["os", "filesystem", "api-bindings"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Cross-platform functionality
|
||||||
|
libc = "0.2"
|
||||||
|
cfg-if = "1.0"
|
||||||
|
thiserror = "1.0" # For error handling
|
||||||
|
log = "0.4" # Logging facade
|
||||||
|
|
||||||
|
# Optional features for specific OS functionality
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
nix = "0.26" # Unix-specific functionality
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.5" # For tests that need temporary files/directories
|
597
src/git/git.rs
Normal file
597
src/git/git.rs
Normal file
@ -0,0 +1,597 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fs;
|
||||||
|
use std::env;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::fmt;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
// Define a custom error type for git operations
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum GitError {
|
||||||
|
GitNotInstalled(std::io::Error),
|
||||||
|
InvalidUrl(String),
|
||||||
|
HomeDirectoryNotFound(std::env::VarError),
|
||||||
|
FileSystemError(std::io::Error),
|
||||||
|
GitCommandFailed(String),
|
||||||
|
CommandExecutionError(std::io::Error),
|
||||||
|
NoRepositoriesFound,
|
||||||
|
RepositoryNotFound(String),
|
||||||
|
MultipleRepositoriesFound(String, usize),
|
||||||
|
NotAGitRepository(String),
|
||||||
|
LocalChangesExist(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Display for GitError
|
||||||
|
impl fmt::Display for GitError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e),
|
||||||
|
GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url),
|
||||||
|
GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e),
|
||||||
|
GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e),
|
||||||
|
GitError::GitCommandFailed(e) => write!(f, "{}", e),
|
||||||
|
GitError::CommandExecutionError(e) => write!(f, "Error executing command: {}", e),
|
||||||
|
GitError::NoRepositoriesFound => write!(f, "No repositories found"),
|
||||||
|
GitError::RepositoryNotFound(pattern) => write!(f, "No repositories found matching '{}'", pattern),
|
||||||
|
GitError::MultipleRepositoriesFound(pattern, count) =>
|
||||||
|
write!(f, "Multiple repositories ({}) found matching '{}'. Use '*' suffix for multiple matches.", count, pattern),
|
||||||
|
GitError::NotAGitRepository(path) => write!(f, "Not a git repository at {}", path),
|
||||||
|
GitError::LocalChangesExist(path) => write!(f, "Repository at {} has local changes", path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Error trait for GitError
|
||||||
|
impl Error for GitError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
GitError::GitNotInstalled(e) => Some(e),
|
||||||
|
GitError::HomeDirectoryNotFound(e) => Some(e),
|
||||||
|
GitError::FileSystemError(e) => Some(e),
|
||||||
|
GitError::CommandExecutionError(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git utility functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones a git repository to a standardized location in the user's home directory.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `url` - The URL of the git repository to clone. Can be in HTTPS format
|
||||||
|
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - The path where the repository was cloned, formatted as
|
||||||
|
* ~/code/server/account/repo (e.g., ~/code/github.com/username/repo).
|
||||||
|
* * `Err(GitError)` - An error if the clone operation failed.
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let repo_path = git_clone("https://github.com/username/repo.git")?;
|
||||||
|
* println!("Repository cloned to: {}", repo_path);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_clone(url: &str) -> Result<String, GitError> {
|
||||||
|
// Check if git is installed
|
||||||
|
let git_check = Command::new("git")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::GitNotInstalled)?;
|
||||||
|
|
||||||
|
// Parse the URL to determine the clone path
|
||||||
|
let (server, account, repo) = parse_git_url(url);
|
||||||
|
if server.is_empty() || account.is_empty() || repo.is_empty() {
|
||||||
|
return Err(GitError::InvalidUrl(url.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the target directory
|
||||||
|
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
|
||||||
|
|
||||||
|
let clone_path = format!("{}/code/{}/{}/{}", home_dir, server, account, repo);
|
||||||
|
let clone_dir = Path::new(&clone_path);
|
||||||
|
|
||||||
|
// Check if repo already exists
|
||||||
|
if clone_dir.exists() {
|
||||||
|
return Ok(format!("Repository already exists at {}", clone_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create parent directory
|
||||||
|
if let Some(parent) = clone_dir.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the repository
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(&["clone", "--depth", "1", url, &clone_path])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(clone_path)
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a git URL to extract the server, account, and repository name.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `url` - The URL of the git repository to parse. Can be in HTTPS format
|
||||||
|
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* A tuple containing:
|
||||||
|
* * `server` - The server name (e.g., "github.com")
|
||||||
|
* * `account` - The account or organization name (e.g., "username")
|
||||||
|
* * `repo` - The repository name (e.g., "repo")
|
||||||
|
*
|
||||||
|
* If the URL cannot be parsed, all three values will be empty strings.
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let (server, account, repo) = parse_git_url("https://github.com/username/repo.git");
|
||||||
|
* assert_eq!(server, "github.com");
|
||||||
|
* assert_eq!(account, "username");
|
||||||
|
* assert_eq!(repo, "repo");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn parse_git_url(url: &str) -> (String, String, String) {
|
||||||
|
// HTTP(S) URL format: https://github.com/username/repo.git
|
||||||
|
let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
|
||||||
|
|
||||||
|
// SSH URL format: git@github.com:username/repo.git
|
||||||
|
let ssh_re = Regex::new(r"git@([^:]+):([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
|
||||||
|
|
||||||
|
if let Some(caps) = https_re.captures(url) {
|
||||||
|
let server = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
||||||
|
let account = caps.get(2).map_or("", |m| m.as_str()).to_string();
|
||||||
|
let repo = caps.get(3).map_or("", |m| m.as_str()).to_string();
|
||||||
|
|
||||||
|
return (server, account, repo);
|
||||||
|
} else if let Some(caps) = ssh_re.captures(url) {
|
||||||
|
let server = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
||||||
|
let account = caps.get(2).map_or("", |m| m.as_str()).to_string();
|
||||||
|
let repo = caps.get(3).map_or("", |m| m.as_str()).to_string();
|
||||||
|
|
||||||
|
return (server, account, repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
(String::new(), String::new(), String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all git repositories found in the user's ~/code directory.
|
||||||
|
*
|
||||||
|
* This function searches for directories containing a .git subdirectory,
|
||||||
|
* which indicates a git repository.
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(Vec<String>)` - A vector of paths to git repositories
|
||||||
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let repos = git_list()?;
|
||||||
|
* for repo in repos {
|
||||||
|
* println!("Found repository: {}", repo);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_list() -> Result<Vec<String>, GitError> {
|
||||||
|
// Get home directory
|
||||||
|
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
|
||||||
|
|
||||||
|
let code_dir = format!("{}/code", home_dir);
|
||||||
|
let code_path = Path::new(&code_dir);
|
||||||
|
|
||||||
|
if !code_path.exists() || !code_path.is_dir() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut repos = Vec::new();
|
||||||
|
|
||||||
|
// Find all directories with .git subdirectories
|
||||||
|
let output = Command::new("find")
|
||||||
|
.args(&[&code_dir, "-type", "d", "-name", ".git"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
// Get the parent directory of .git which is the repo root
|
||||||
|
if let Some(parent) = Path::new(line).parent() {
|
||||||
|
if let Some(path_str) = parent.to_str() {
|
||||||
|
repos.push(path_str.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a git repository has uncommitted changes.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
|
||||||
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* if has_git_changes("/path/to/repo")? {
|
||||||
|
* println!("Repository has uncommitted changes");
|
||||||
|
* } else {
|
||||||
|
* println!("Repository is clean");
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn has_git_changes(repo_path: &str) -> Result<bool, GitError> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(&["-C", repo_path, "status", "--porcelain"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
Ok(!output.stdout.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds repositories matching a pattern or partial path.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `pattern` - The pattern to match against repository paths
|
||||||
|
* - If the pattern ends with '*', all matching repositories are returned
|
||||||
|
* - Otherwise, exactly one matching repository must be found
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(Vec<String>)` - A vector of paths to matching repositories
|
||||||
|
* * `Err(GitError)` - An error if no matching repositories are found,
|
||||||
|
* or if multiple repositories match a non-wildcard pattern
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Find all repositories containing "project"
|
||||||
|
* let repos = find_matching_repos("project*")?;
|
||||||
|
*
|
||||||
|
* // Find exactly one repository containing "unique-project"
|
||||||
|
* let repo = find_matching_repos("unique-project")?[0];
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn find_matching_repos(pattern: &str) -> Result<Vec<String>, GitError> {
|
||||||
|
// Get all repos
|
||||||
|
let repos = git_list()?;
|
||||||
|
|
||||||
|
if repos.is_empty() {
|
||||||
|
return Err(GitError::NoRepositoriesFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if pattern ends with wildcard
|
||||||
|
if pattern.ends_with('*') {
|
||||||
|
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
|
||||||
|
let matching: Vec<String> = repos.iter()
|
||||||
|
.filter(|repo| repo.contains(search_pattern))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if matching.is_empty() {
|
||||||
|
return Err(GitError::RepositoryNotFound(pattern.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(matching)
|
||||||
|
} else {
|
||||||
|
// No wildcard, need to find exactly one match
|
||||||
|
let matching: Vec<String> = repos.iter()
|
||||||
|
.filter(|repo| repo.contains(pattern))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match matching.len() {
|
||||||
|
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
|
||||||
|
1 => Ok(matching),
|
||||||
|
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a git repository by pulling the latest changes.
|
||||||
|
*
|
||||||
|
* This function will fail if there are uncommitted changes in the repository.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was updated
|
||||||
|
* * `Err(GitError)` - An error if the update failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update("my-project")?;
|
||||||
|
* println!("{}", result); // "Successfully updated repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update(repo_path: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
|
// Should only be one repository at this point
|
||||||
|
let actual_path = &repos[0];
|
||||||
|
|
||||||
|
// Check if repository exists and is a git repository
|
||||||
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for local changes
|
||||||
|
if has_git_changes(actual_path)? {
|
||||||
|
return Err(GitError::LocalChangesExist(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the latest changes
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "pull"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if stdout.contains("Already up to date") {
|
||||||
|
Ok(format!("Repository already up to date at {}", actual_path))
|
||||||
|
} else {
|
||||||
|
Ok(format!("Successfully updated repository at {}", actual_path))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force updates a git repository by discarding local changes and pulling the latest changes.
|
||||||
|
*
|
||||||
|
* This function will reset any uncommitted changes and clean untracked files before pulling.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was force-updated
|
||||||
|
* * `Err(GitError)` - An error if the update failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update_force("my-project")?;
|
||||||
|
* println!("{}", result); // "Successfully force-updated repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update_force(repo_path: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
|
// Should only be one repository at this point
|
||||||
|
let actual_path = &repos[0];
|
||||||
|
|
||||||
|
// Check if repository exists and is a git repository
|
||||||
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset any local changes
|
||||||
|
let reset_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "reset", "--hard", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !reset_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&reset_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean untracked files
|
||||||
|
let clean_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "clean", "-fd"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !clean_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&clean_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the latest changes
|
||||||
|
let pull_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "pull"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if pull_output.status.success() {
|
||||||
|
Ok(format!("Successfully force-updated repository at {}", actual_path))
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&pull_output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits changes in a git repository and then updates it by pulling the latest changes.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
* * `message` - The commit message
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was committed and updated
|
||||||
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update_commit("my-project", "Fix bug in login form")?;
|
||||||
|
* println!("{}", result); // "Successfully committed and updated repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
|
// Should only be one repository at this point
|
||||||
|
let actual_path = &repos[0];
|
||||||
|
|
||||||
|
// Check if repository exists and is a git repository
|
||||||
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for local changes
|
||||||
|
if !has_git_changes(actual_path)? {
|
||||||
|
return Ok(format!("No changes to commit in repository at {}", actual_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all changes
|
||||||
|
let add_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "add", "."])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !add_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the changes
|
||||||
|
let commit_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "commit", "-m", message])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !commit_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the latest changes
|
||||||
|
let pull_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "pull"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if pull_output.status.success() {
|
||||||
|
Ok(format!("Successfully committed and updated repository at {}", actual_path))
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&pull_output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits changes in a git repository and pushes them to the remote.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||||
|
* * `message` - The commit message
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating the repository was committed and pushed
|
||||||
|
* * `Err(GitError)` - An error if the operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = git_update_commit_push("my-project", "Add new feature")?;
|
||||||
|
* println!("{}", result); // "Successfully committed and pushed repository at /home/user/code/github.com/user/my-project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, GitError> {
|
||||||
|
// If repo_path may be a partial path, find the matching repository
|
||||||
|
let repos = find_matching_repos(repo_path)?;
|
||||||
|
|
||||||
|
// Should only be one repository at this point
|
||||||
|
let actual_path = &repos[0];
|
||||||
|
|
||||||
|
// Check if repository exists and is a git repository
|
||||||
|
let git_dir = Path::new(actual_path).join(".git");
|
||||||
|
if !git_dir.exists() || !git_dir.is_dir() {
|
||||||
|
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for local changes
|
||||||
|
if !has_git_changes(actual_path)? {
|
||||||
|
return Ok(format!("No changes to commit in repository at {}", actual_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all changes
|
||||||
|
let add_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "add", "."])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !add_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the changes
|
||||||
|
let commit_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "commit", "-m", message])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if !commit_output.status.success() {
|
||||||
|
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||||
|
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the changes
|
||||||
|
let push_output = Command::new("git")
|
||||||
|
.args(&["-C", actual_path, "push"])
|
||||||
|
.output()
|
||||||
|
.map_err(GitError::CommandExecutionError)?;
|
||||||
|
|
||||||
|
if push_output.status.success() {
|
||||||
|
Ok(format!("Successfully committed and pushed repository at {}", actual_path))
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&push_output.stderr);
|
||||||
|
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
7
src/git/git_executor.rs
Normal file
7
src/git/git_executor.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fs;
|
||||||
|
use std::env;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::fmt;
|
||||||
|
use std::error::Error;
|
12
src/git/instructions.md
Normal file
12
src/git/instructions.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
in @/sal/git/git_executor.rs
|
||||||
|
create a GitExecutor which is the one executing git commands
|
||||||
|
and also checking if ssh-agent is loaded
|
||||||
|
or if there is an authentication mechanism defined in redis
|
||||||
|
|
||||||
|
check if there is a redis on ~/hero/var/redis.sock
|
||||||
|
|
||||||
|
over unix domani socket
|
||||||
|
|
||||||
|
if yes then check there is an entry on
|
||||||
|
|
||||||
|
sal::git::
|
2
src/git/mod.rs
Normal file
2
src/git/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
use git::*;
|
||||||
|
use git_executor::*;
|
60
src/lib.rs
Normal file
60
src/lib.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
//! # System Abstraction Layer (SAL)
|
||||||
|
//!
|
||||||
|
//! `sal` is a library that provides a unified interface for interacting with
|
||||||
|
//! operating system features across different platforms. It abstracts away the
|
||||||
|
//! platform-specific details, allowing developers to write cross-platform code
|
||||||
|
//! with ease.
|
||||||
|
//!
|
||||||
|
//! ## Features
|
||||||
|
//!
|
||||||
|
//! - File system operations
|
||||||
|
//! - Process management
|
||||||
|
//! - System information
|
||||||
|
//! - Network operations
|
||||||
|
//! - Environment variables
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Error types for the SAL library
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// An error occurred during an I/O operation
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
|
||||||
|
/// An error specific to the SAL library
|
||||||
|
#[error("SAL error: {0}")]
|
||||||
|
Sal(String),
|
||||||
|
|
||||||
|
/// An error that occurred in a platform-specific operation
|
||||||
|
#[error("Platform-specific error: {0}")]
|
||||||
|
Platform(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type for SAL operations
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
// Re-export modules
|
||||||
|
pub mod process;
|
||||||
|
pub mod git;
|
||||||
|
pub mod os;
|
||||||
|
pub mod network;
|
||||||
|
pub mod env;
|
||||||
|
pub mod text;
|
||||||
|
|
||||||
|
// Version information
|
||||||
|
/// Returns the version of the SAL library
|
||||||
|
pub fn version() -> &'static str {
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version() {
|
||||||
|
assert!(!version().is_empty());
|
||||||
|
}
|
||||||
|
}
|
3
src/mod.rs
Normal file
3
src/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod os;
|
||||||
|
pub mod text;
|
||||||
|
pub mod git;
|
289
src/os/download.rs
Normal file
289
src/os/download.rs
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fs;
|
||||||
|
use std::fmt;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
// Define a custom error type for download operations
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DownloadError {
|
||||||
|
CreateDirectoryFailed(io::Error),
|
||||||
|
CurlExecutionFailed(io::Error),
|
||||||
|
DownloadFailed(String),
|
||||||
|
FileMetadataError(io::Error),
|
||||||
|
FileTooSmall(i64, i64),
|
||||||
|
RemoveFileFailed(io::Error),
|
||||||
|
ExtractionFailed(String),
|
||||||
|
CommandExecutionFailed(io::Error),
|
||||||
|
InvalidUrl(String),
|
||||||
|
NotAFile(String),
|
||||||
|
PlatformNotSupported(String),
|
||||||
|
InstallationFailed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Display for DownloadError
|
||||||
|
impl fmt::Display for DownloadError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e),
|
||||||
|
DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
|
||||||
|
DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
|
||||||
|
DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e),
|
||||||
|
DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min),
|
||||||
|
DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
|
||||||
|
DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
|
||||||
|
DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
|
||||||
|
DownloadError::InvalidUrl(url) => write!(f, "Invalid URL: {}", url),
|
||||||
|
DownloadError::NotAFile(path) => write!(f, "Not a file: {}", path),
|
||||||
|
DownloadError::PlatformNotSupported(msg) => write!(f, "{}", msg),
|
||||||
|
DownloadError::InstallationFailed(msg) => write!(f, "{}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Error trait for DownloadError
|
||||||
|
impl Error for DownloadError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
DownloadError::CreateDirectoryFailed(e) => Some(e),
|
||||||
|
DownloadError::CurlExecutionFailed(e) => Some(e),
|
||||||
|
DownloadError::FileMetadataError(e) => Some(e),
|
||||||
|
DownloadError::RemoveFileFailed(e) => Some(e),
|
||||||
|
DownloadError::CommandExecutionFailed(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a file from URL to destination using the curl command.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `url` - The URL to download from
|
||||||
|
* * `dest` - The destination path where the file will be saved
|
||||||
|
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - The path where the file was saved or extracted
|
||||||
|
* * `Err(DownloadError)` - An error if the download failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Download a file with no minimum size requirement
|
||||||
|
* let path = download("https://example.com/file.txt", "/tmp/file.txt", 0)?;
|
||||||
|
*
|
||||||
|
* // Download a file with minimum size requirement of 100KB
|
||||||
|
* let path = download("https://example.com/file.zip", "/tmp/file.zip", 100)?;
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* # Notes
|
||||||
|
*
|
||||||
|
* If the URL ends with .tar.gz, .tgz, .tar, or .zip, the file will be automatically
|
||||||
|
* extracted to the destination directory.
|
||||||
|
*/
|
||||||
|
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
|
||||||
|
// Create parent directories if they don't exist
|
||||||
|
let dest_path = Path::new(dest);
|
||||||
|
if let Some(parent) = dest_path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary path for downloading
|
||||||
|
let temp_path = format!("{}.download", dest);
|
||||||
|
|
||||||
|
// Use curl to download the file with progress bar
|
||||||
|
println!("Downloading {} to {}", url, dest);
|
||||||
|
let output = Command::new("curl")
|
||||||
|
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
|
||||||
|
.status()
|
||||||
|
.map_err(DownloadError::CurlExecutionFailed)?;
|
||||||
|
|
||||||
|
if !output.success() {
|
||||||
|
return Err(DownloadError::DownloadFailed(url.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show file size after download
|
||||||
|
match fs::metadata(&temp_path) {
|
||||||
|
Ok(metadata) => {
|
||||||
|
let size_bytes = metadata.len();
|
||||||
|
let size_kb = size_bytes / 1024;
|
||||||
|
let size_mb = size_kb / 1024;
|
||||||
|
if size_mb > 1 {
|
||||||
|
println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0));
|
||||||
|
} else {
|
||||||
|
println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => println!("Download complete!"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size if minimum size is specified
|
||||||
|
if min_size_kb > 0 {
|
||||||
|
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
|
||||||
|
let size_kb = metadata.len() as i64 / 1024;
|
||||||
|
if size_kb < min_size_kb {
|
||||||
|
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
|
||||||
|
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a compressed file that needs extraction
|
||||||
|
let lower_url = url.to_lowercase();
|
||||||
|
let is_archive = lower_url.ends_with(".tar.gz") ||
|
||||||
|
lower_url.ends_with(".tgz") ||
|
||||||
|
lower_url.ends_with(".tar") ||
|
||||||
|
lower_url.ends_with(".zip");
|
||||||
|
|
||||||
|
if is_archive {
|
||||||
|
// Create the destination directory
|
||||||
|
fs::create_dir_all(dest).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||||
|
|
||||||
|
// Extract the file using the appropriate command with progress indication
|
||||||
|
println!("Extracting {} to {}", temp_path, dest);
|
||||||
|
let output = if lower_url.ends_with(".zip") {
|
||||||
|
Command::new("unzip")
|
||||||
|
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
|
||||||
|
.status()
|
||||||
|
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
|
||||||
|
Command::new("tar")
|
||||||
|
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||||
|
.status()
|
||||||
|
} else {
|
||||||
|
Command::new("tar")
|
||||||
|
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||||
|
.status()
|
||||||
|
};
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(status) => {
|
||||||
|
if !status.success() {
|
||||||
|
return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show number of extracted files
|
||||||
|
match fs::read_dir(dest) {
|
||||||
|
Ok(entries) => {
|
||||||
|
let count = entries.count();
|
||||||
|
println!("Extraction complete! Extracted {} files/directories", count);
|
||||||
|
},
|
||||||
|
Err(_) => println!("Extraction complete!"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the temporary file
|
||||||
|
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
|
||||||
|
|
||||||
|
Ok(dest.to_string())
|
||||||
|
} else {
|
||||||
|
// Just rename the temporary file to the final destination
|
||||||
|
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||||
|
|
||||||
|
Ok(dest.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a file and install it if it's a supported package format.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `url` - The URL to download from
|
||||||
|
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - The path where the file was saved or extracted
|
||||||
|
* * `Err(DownloadError)` - An error if the download or installation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Download and install a .deb package
|
||||||
|
* let result = download_install("https://example.com/package.deb", 100)?;
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* # Notes
|
||||||
|
*
|
||||||
|
* Currently only supports .deb packages on Debian-based systems.
|
||||||
|
* For other file types, it behaves the same as the download function.
|
||||||
|
*/
|
||||||
|
pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadError> {
|
||||||
|
// Extract filename from URL
|
||||||
|
let filename = match url.split('/').last() {
|
||||||
|
Some(name) => name,
|
||||||
|
None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string()))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a proper destination path
|
||||||
|
let dest_path = format!("/tmp/{}", filename);
|
||||||
|
|
||||||
|
let download_result = download(url, &dest_path, min_size_kb)?;
|
||||||
|
|
||||||
|
// Check if the downloaded result is a file
|
||||||
|
let path = Path::new(&dest_path);
|
||||||
|
if !path.is_file() {
|
||||||
|
return Ok(download_result); // Not a file, might be an extracted directory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a .deb package
|
||||||
|
if dest_path.to_lowercase().ends_with(".deb") {
|
||||||
|
// Check if we're on a Debian-based platform
|
||||||
|
let platform_check = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match platform_check {
|
||||||
|
Ok(status) => {
|
||||||
|
if !status.success() {
|
||||||
|
return Err(DownloadError::PlatformNotSupported(
|
||||||
|
"Cannot install .deb package: not on a Debian-based system".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => return Err(DownloadError::PlatformNotSupported(
|
||||||
|
"Failed to check system compatibility for .deb installation".to_string()
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the .deb package non-interactively
|
||||||
|
println!("Installing package: {}", dest_path);
|
||||||
|
let install_result = Command::new("sudo")
|
||||||
|
.args(&["dpkg", "--install", &dest_path])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match install_result {
|
||||||
|
Ok(status) => {
|
||||||
|
if !status.success() {
|
||||||
|
// If dpkg fails, try to fix dependencies and retry
|
||||||
|
println!("Attempting to resolve dependencies...");
|
||||||
|
let fix_deps = Command::new("sudo")
|
||||||
|
.args(&["apt-get", "install", "-f", "-y"])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
if let Ok(fix_status) = fix_deps {
|
||||||
|
if !fix_status.success() {
|
||||||
|
return Err(DownloadError::InstallationFailed(
|
||||||
|
"Failed to resolve package dependencies".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(DownloadError::InstallationFailed(
|
||||||
|
"Failed to resolve package dependencies".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Package installation completed successfully");
|
||||||
|
},
|
||||||
|
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(download_result)
|
||||||
|
}
|
580
src/os/fs.rs
Normal file
580
src/os/fs.rs
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
_ => 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
|
||||||
|
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<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)),
|
||||||
|
}
|
||||||
|
}
|
2
src/os/mod.rs
Normal file
2
src/os/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
use fs::*;
|
||||||
|
use download::*;
|
303
src/process/mgmt.rs
Normal file
303
src/process/mgmt.rs
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
// Define a custom error type for process operations
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ProcessError {
|
||||||
|
CommandExecutionFailed(io::Error),
|
||||||
|
CommandFailed(String),
|
||||||
|
NoProcessFound(String),
|
||||||
|
MultipleProcessesFound(String, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Display for ProcessError
|
||||||
|
impl fmt::Display for ProcessError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ProcessError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
|
||||||
|
ProcessError::CommandFailed(e) => write!(f, "{}", e),
|
||||||
|
ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern),
|
||||||
|
ProcessError::MultipleProcessesFound(pattern, count) =>
|
||||||
|
write!(f, "Multiple processes ({}) found matching '{}'", count, pattern),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Error trait for ProcessError
|
||||||
|
impl Error for ProcessError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
ProcessError::CommandExecutionFailed(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a struct to represent process information
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProcessInfo {
|
||||||
|
pub pid: i64,
|
||||||
|
pub name: String,
|
||||||
|
pub memory: f64,
|
||||||
|
pub cpu: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a command exists in PATH.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `cmd` - The command to check
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Option<String>` - The full path to the command if found, None otherwise
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* match which("git") {
|
||||||
|
* Some(path) => println!("Git is installed at: {}", path),
|
||||||
|
* None => println!("Git is not installed"),
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn which(cmd: &str) -> Option<String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let which_cmd = "where";
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
let which_cmd = "which";
|
||||||
|
|
||||||
|
let output = Command::new(which_cmd)
|
||||||
|
.arg(cmd)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(out) => {
|
||||||
|
if out.status.success() {
|
||||||
|
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill processes matching a pattern.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `pattern` - The pattern to match against process names
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(String)` - A success message indicating processes were killed or none were found
|
||||||
|
* * `Err(ProcessError)` - An error if the kill operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Kill all processes with "server" in their name
|
||||||
|
* let result = kill("server")?;
|
||||||
|
* println!("{}", result);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
||||||
|
// Platform specific implementation
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// On Windows, use taskkill with wildcard support
|
||||||
|
let mut args = vec!["/F"]; // Force kill
|
||||||
|
|
||||||
|
if pattern.contains('*') {
|
||||||
|
// If it contains wildcards, use filter
|
||||||
|
args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
|
||||||
|
} else {
|
||||||
|
// Otherwise use image name directly
|
||||||
|
args.extend(&["/IM", pattern]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new("taskkill")
|
||||||
|
.args(&args)
|
||||||
|
.output()
|
||||||
|
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok("Successfully killed processes".to_string())
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
if error.is_empty() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if stdout.contains("No tasks") {
|
||||||
|
Ok("No matching processes found".to_string())
|
||||||
|
} else {
|
||||||
|
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
{
|
||||||
|
// On Unix-like systems, use pkill which has built-in pattern matching
|
||||||
|
let output = Command::new("pkill")
|
||||||
|
.arg("-f") // Match against full process name/args
|
||||||
|
.arg(pattern)
|
||||||
|
.output()
|
||||||
|
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||||
|
|
||||||
|
// pkill returns 0 if processes were killed, 1 if none matched
|
||||||
|
if output.status.success() {
|
||||||
|
Ok("Successfully killed processes".to_string())
|
||||||
|
} else if output.status.code() == Some(1) {
|
||||||
|
Ok("No matching processes found".to_string())
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List processes matching a pattern (or all if pattern is empty).
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `pattern` - The pattern to match against process names (empty string for all processes)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
|
||||||
|
* * `Err(ProcessError)` - An error if the list operation failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // List all processes
|
||||||
|
* let processes = process_list("")?;
|
||||||
|
*
|
||||||
|
* // List processes with "server" in their name
|
||||||
|
* let processes = process_list("server")?;
|
||||||
|
* for proc in processes {
|
||||||
|
* println!("PID: {}, Name: {}", proc.pid, proc.name);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
|
||||||
|
let mut processes = Vec::new();
|
||||||
|
|
||||||
|
// Platform specific implementations
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Windows implementation using wmic
|
||||||
|
let output = Command::new("wmic")
|
||||||
|
.args(&["process", "list", "brief"])
|
||||||
|
.output()
|
||||||
|
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
|
|
||||||
|
// Parse output (assuming format: Handle Name Priority)
|
||||||
|
for line in stdout.lines().skip(1) { // Skip header
|
||||||
|
let parts: Vec<&str> = line.trim().split_whitespace().collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
let pid = parts[0].parse::<i64>().unwrap_or(0);
|
||||||
|
let name = parts[1].to_string();
|
||||||
|
|
||||||
|
// Filter by pattern if provided
|
||||||
|
if !pattern.is_empty() && !name.contains(pattern) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
processes.push(ProcessInfo {
|
||||||
|
pid,
|
||||||
|
name,
|
||||||
|
memory: 0.0, // Placeholder
|
||||||
|
cpu: 0.0, // Placeholder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
{
|
||||||
|
// Unix implementation using ps
|
||||||
|
let output = Command::new("ps")
|
||||||
|
.args(&["-eo", "pid,comm"])
|
||||||
|
.output()
|
||||||
|
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
|
|
||||||
|
// Parse output (assuming format: PID COMMAND)
|
||||||
|
for line in stdout.lines().skip(1) { // Skip header
|
||||||
|
let parts: Vec<&str> = line.trim().split_whitespace().collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
let pid = parts[0].parse::<i64>().unwrap_or(0);
|
||||||
|
let name = parts[1].to_string();
|
||||||
|
|
||||||
|
// Filter by pattern if provided
|
||||||
|
if !pattern.is_empty() && !name.contains(pattern) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
processes.push(ProcessInfo {
|
||||||
|
pid,
|
||||||
|
name,
|
||||||
|
memory: 0.0, // Placeholder
|
||||||
|
cpu: 0.0, // Placeholder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(processes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single process matching the pattern (error if 0 or more than 1 match).
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `pattern` - The pattern to match against process names
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(ProcessInfo)` - Information about the matching process
|
||||||
|
* * `Err(ProcessError)` - An error if no process or multiple processes match
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let process = process_get("unique-server-name")?;
|
||||||
|
* println!("Found process: {} (PID: {})", process.name, process.pid);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
|
||||||
|
let processes = process_list(pattern)?;
|
||||||
|
|
||||||
|
match processes.len() {
|
||||||
|
0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
|
||||||
|
1 => Ok(processes[0].clone()),
|
||||||
|
_ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())),
|
||||||
|
}
|
||||||
|
}
|
2
src/process/mod.rs
Normal file
2
src/process/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
use run::*;
|
||||||
|
use mgmt::*;
|
493
src/process/run.rs
Normal file
493
src/process/run.rs
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Child, Command, Output, Stdio};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use super::text;
|
||||||
|
|
||||||
|
// Define a custom error type for run operations
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RunError {
|
||||||
|
EmptyCommand,
|
||||||
|
CommandExecutionFailed(io::Error),
|
||||||
|
CommandFailed(String),
|
||||||
|
ScriptPreparationFailed(String),
|
||||||
|
ChildProcessError(String),
|
||||||
|
TempDirCreationFailed(io::Error),
|
||||||
|
FileCreationFailed(io::Error),
|
||||||
|
FileWriteFailed(io::Error),
|
||||||
|
PermissionError(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Display for RunError
|
||||||
|
impl fmt::Display for RunError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
RunError::EmptyCommand => write!(f, "Empty command"),
|
||||||
|
RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
|
||||||
|
RunError::CommandFailed(e) => write!(f, "{}", e),
|
||||||
|
RunError::ScriptPreparationFailed(e) => write!(f, "{}", e),
|
||||||
|
RunError::ChildProcessError(e) => write!(f, "{}", e),
|
||||||
|
RunError::TempDirCreationFailed(e) => write!(f, "Failed to create temporary directory: {}", e),
|
||||||
|
RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e),
|
||||||
|
RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e),
|
||||||
|
RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Error trait for RunError
|
||||||
|
impl Error for RunError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
RunError::CommandExecutionFailed(e) => Some(e),
|
||||||
|
RunError::TempDirCreationFailed(e) => Some(e),
|
||||||
|
RunError::FileCreationFailed(e) => Some(e),
|
||||||
|
RunError::FileWriteFailed(e) => Some(e),
|
||||||
|
RunError::PermissionError(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure to hold command execution results
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CommandResult {
|
||||||
|
pub stdout: String,
|
||||||
|
pub stderr: String,
|
||||||
|
pub success: bool,
|
||||||
|
pub code: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandResult {
|
||||||
|
/// Create a default failed result with an error message
|
||||||
|
fn error(message: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
stdout: String::new(),
|
||||||
|
stderr: message.to_string(),
|
||||||
|
success: false,
|
||||||
|
code: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare a script file and return the path and interpreter
|
||||||
|
fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfile::TempDir), RunError> {
|
||||||
|
// Dedent the script
|
||||||
|
let dedented = text::dedent(script_content);
|
||||||
|
|
||||||
|
// Create a temporary directory
|
||||||
|
let temp_dir = tempfile::tempdir()
|
||||||
|
.map_err(RunError::TempDirCreationFailed)?;
|
||||||
|
|
||||||
|
// Determine script extension and interpreter
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let (ext, interpreter) = (".bat", "cmd.exe".to_string());
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
let (ext, interpreter) = (".sh", "/bin/sh".to_string());
|
||||||
|
|
||||||
|
// Create the script file
|
||||||
|
let script_path = temp_dir.path().join(format!("script{}", ext));
|
||||||
|
let mut file = File::create(&script_path)
|
||||||
|
.map_err(RunError::FileCreationFailed)?;
|
||||||
|
|
||||||
|
// Write the script content
|
||||||
|
file.write_all(dedented.as_bytes())
|
||||||
|
.map_err(RunError::FileWriteFailed)?;
|
||||||
|
|
||||||
|
// Make the script executable (Unix only)
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(&script_path)
|
||||||
|
.map_err(|e| RunError::PermissionError(e))?
|
||||||
|
.permissions();
|
||||||
|
perms.set_mode(0o755); // rwxr-xr-x
|
||||||
|
fs::set_permissions(&script_path, perms)
|
||||||
|
.map_err(RunError::PermissionError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((script_path, interpreter, temp_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capture output from Child's stdio streams with optional printing
|
||||||
|
fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult, RunError> {
|
||||||
|
// Prepare to read stdout & stderr line-by-line
|
||||||
|
let stdout = child.stdout.take();
|
||||||
|
let stderr = child.stderr.take();
|
||||||
|
|
||||||
|
// Buffers for captured output
|
||||||
|
let mut captured_stdout = String::new();
|
||||||
|
let mut captured_stderr = String::new();
|
||||||
|
|
||||||
|
// Process stdout
|
||||||
|
let stdout_handle = if let Some(out) = stdout {
|
||||||
|
let reader = BufReader::new(out);
|
||||||
|
let silent_clone = silent;
|
||||||
|
// Spawn a thread to capture and optionally print stdout
|
||||||
|
Some(std::thread::spawn(move || {
|
||||||
|
let mut local_buffer = String::new();
|
||||||
|
for line in reader.lines() {
|
||||||
|
if let Ok(l) = line {
|
||||||
|
// Print the line if not silent and flush immediately
|
||||||
|
if !silent_clone {
|
||||||
|
println!("{}", l);
|
||||||
|
std::io::stdout().flush().unwrap_or(());
|
||||||
|
}
|
||||||
|
// Store it in our captured buffer
|
||||||
|
local_buffer.push_str(&l);
|
||||||
|
local_buffer.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_buffer
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process stderr
|
||||||
|
let stderr_handle = if let Some(err) = stderr {
|
||||||
|
let reader = BufReader::new(err);
|
||||||
|
let silent_clone = silent;
|
||||||
|
// Spawn a thread to capture and optionally print stderr
|
||||||
|
Some(std::thread::spawn(move || {
|
||||||
|
let mut local_buffer = String::new();
|
||||||
|
for line in reader.lines() {
|
||||||
|
if let Ok(l) = line {
|
||||||
|
// Print the line if not silent and flush immediately
|
||||||
|
if !silent_clone {
|
||||||
|
eprintln!("{}", l);
|
||||||
|
std::io::stderr().flush().unwrap_or(());
|
||||||
|
}
|
||||||
|
// Store it in our captured buffer
|
||||||
|
local_buffer.push_str(&l);
|
||||||
|
local_buffer.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_buffer
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for the child process to exit
|
||||||
|
let status = child.wait()
|
||||||
|
.map_err(|e| RunError::ChildProcessError(format!("Failed to wait on child process: {}", e)))?;
|
||||||
|
|
||||||
|
// Join our stdout thread if it exists
|
||||||
|
if let Some(handle) = stdout_handle {
|
||||||
|
captured_stdout = handle.join().unwrap_or_default();
|
||||||
|
} else {
|
||||||
|
captured_stdout = "Failed to capture stdout".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join our stderr thread if it exists
|
||||||
|
if let Some(handle) = stderr_handle {
|
||||||
|
captured_stderr = handle.join().unwrap_or_default();
|
||||||
|
} else {
|
||||||
|
captured_stderr = "Failed to capture stderr".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the command result
|
||||||
|
Ok(CommandResult {
|
||||||
|
stdout: captured_stdout,
|
||||||
|
stderr: captured_stderr,
|
||||||
|
success: status.success(),
|
||||||
|
code: status.code().unwrap_or(-1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes Output structure from Command::output() into CommandResult
|
||||||
|
fn process_command_output(output: Result<Output, std::io::Error>) -> Result<CommandResult, RunError> {
|
||||||
|
match output {
|
||||||
|
Ok(out) => {
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||||
|
|
||||||
|
Ok(CommandResult {
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
success: out.status.success(),
|
||||||
|
code: out.status.code().unwrap_or(-1),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Err(e) => Err(RunError::CommandExecutionFailed(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common logic for running a command with optional silent mode.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `command` - The command + args as a single string (e.g., "ls -la")
|
||||||
|
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the command execution
|
||||||
|
* * `Err(RunError)` - An error if the command execution failed
|
||||||
|
*/
|
||||||
|
fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
|
||||||
|
let mut parts = command.split_whitespace();
|
||||||
|
let cmd = match parts.next() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(RunError::EmptyCommand),
|
||||||
|
};
|
||||||
|
|
||||||
|
let args: Vec<&str> = parts.collect();
|
||||||
|
|
||||||
|
// Spawn the child process with piped stdout & stderr
|
||||||
|
let child = Command::new(cmd)
|
||||||
|
.args(&args)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(RunError::CommandExecutionFailed)?;
|
||||||
|
|
||||||
|
handle_child_output(child, silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a script with the given interpreter and path.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `interpreter` - The interpreter to use (e.g., "/bin/sh")
|
||||||
|
* * `script_path` - The path to the script file
|
||||||
|
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the script execution
|
||||||
|
* * `Err(RunError)` - An error if the script execution failed
|
||||||
|
*/
|
||||||
|
fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) -> Result<CommandResult, RunError> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
let command_args = vec![script_path.to_str().unwrap_or("")];
|
||||||
|
|
||||||
|
if silent {
|
||||||
|
// For silent execution, use output() which captures but doesn't display
|
||||||
|
let output = Command::new(interpreter)
|
||||||
|
.args(&command_args)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
process_command_output(output)
|
||||||
|
} else {
|
||||||
|
// For normal execution, spawn and handle the output streams
|
||||||
|
let child = Command::new(interpreter)
|
||||||
|
.args(&command_args)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(RunError::CommandExecutionFailed)?;
|
||||||
|
|
||||||
|
handle_child_output(child, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single command with arguments, showing live stdout and stderr.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `command` - The command + args as a single string (e.g., "ls -la")
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the command execution
|
||||||
|
* * `Err(RunError)` - An error if the command execution failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = run_command("ls -la")?;
|
||||||
|
* println!("Command exited with code: {}", result.code);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
|
||||||
|
run_command_internal(command, /* silent = */ false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single command with arguments silently.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `command` - The command + args as a single string (e.g., "ls -la")
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the command execution
|
||||||
|
* * `Err(RunError)` - An error if the command execution failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let result = run_command_silent("ls -la")?;
|
||||||
|
* println!("Command output: {}", result.stdout);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn run_command_silent(command: &str) -> Result<CommandResult, RunError> {
|
||||||
|
run_command_internal(command, /* silent = */ true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a multiline script with optional silent mode.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `script` - The script content as a string
|
||||||
|
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the script execution
|
||||||
|
* * `Err(RunError)` - An error if the script execution failed
|
||||||
|
*/
|
||||||
|
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
|
||||||
|
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
|
||||||
|
// _temp_dir is kept in scope until the end of this function to ensure
|
||||||
|
// it's not dropped prematurely, which would clean up the directory
|
||||||
|
execute_script_internal(&interpreter, &script_path, silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a multiline script by saving it to a temporary file and executing.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `script` - The script content as a string
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the script execution
|
||||||
|
* * `Err(RunError)` - An error if the script execution failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let script = r#"
|
||||||
|
* echo "Hello, world!"
|
||||||
|
* ls -la
|
||||||
|
* "#;
|
||||||
|
* let result = run_script(script)?;
|
||||||
|
* println!("Script exited with code: {}", result.code);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn run_script(script: &str) -> Result<CommandResult, RunError> {
|
||||||
|
run_script_internal(script, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a multiline script silently by saving it to a temporary file and executing.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `script` - The script content as a string
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the script execution
|
||||||
|
* * `Err(RunError)` - An error if the script execution failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let script = r#"
|
||||||
|
* echo "Hello, world!"
|
||||||
|
* ls -la
|
||||||
|
* "#;
|
||||||
|
* let result = run_script_silent(script)?;
|
||||||
|
* println!("Script output: {}", result.stdout);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn run_script_silent(script: &str) -> Result<CommandResult, RunError> {
|
||||||
|
run_script_internal(script, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a command or multiline script with arguments.
|
||||||
|
* Shows stdout/stderr as it arrives.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `command` - The command or script to run
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the execution
|
||||||
|
* * `Err(RunError)` - An error if the execution failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Run a single command
|
||||||
|
* let result = run("ls -la")?;
|
||||||
|
*
|
||||||
|
* // Run a multiline script
|
||||||
|
* let result = run(r#"
|
||||||
|
* echo "Hello, world!"
|
||||||
|
* ls -la
|
||||||
|
* "#)?;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn run(command: &str) -> Result<CommandResult, RunError> {
|
||||||
|
let trimmed = command.trim();
|
||||||
|
|
||||||
|
// Check if this is a multiline script
|
||||||
|
if trimmed.contains('\n') {
|
||||||
|
// This is a multiline script, write to a temporary file and execute
|
||||||
|
run_script(trimmed)
|
||||||
|
} else {
|
||||||
|
// This is a single command with arguments
|
||||||
|
run_command(trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a command or multiline script with arguments silently.
|
||||||
|
* Doesn't show stdout/stderr as it arrives.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `command` - The command or script to run
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `Ok(CommandResult)` - The result of the execution
|
||||||
|
* * `Err(RunError)` - An error if the execution failed
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Run a single command silently
|
||||||
|
* let result = run_silent("ls -la")?;
|
||||||
|
*
|
||||||
|
* // Run a multiline script silently
|
||||||
|
* let result = run_silent(r#"
|
||||||
|
* echo "Hello, world!"
|
||||||
|
* ls -la
|
||||||
|
* "#)?;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
|
||||||
|
let trimmed = command.trim();
|
||||||
|
|
||||||
|
// Check if this is a multiline script
|
||||||
|
if trimmed.contains('\n') {
|
||||||
|
// This is a multiline script, write to a temporary file and execute
|
||||||
|
run_script_silent(trimmed)
|
||||||
|
} else {
|
||||||
|
// This is a single command with arguments
|
||||||
|
run_command_silent(trimmed)
|
||||||
|
}
|
||||||
|
}
|
1
src/text/mod.rs
Normal file
1
src/text/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
use text::*;
|
83
src/text/text.rs
Normal file
83
src/text/text.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Dedent a multiline string by removing common leading whitespace.
|
||||||
|
*
|
||||||
|
* This function analyzes all non-empty lines in the input text to determine
|
||||||
|
* the minimum indentation level, then removes that amount of whitespace
|
||||||
|
* from the beginning of each line. This is useful for working with
|
||||||
|
* multi-line strings in code that have been indented to match the
|
||||||
|
* surrounding code structure.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
*
|
||||||
|
* * `text` - The multiline string to dedent
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
*
|
||||||
|
* * `String` - The dedented string
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* let indented = " line 1\n line 2\n line 3";
|
||||||
|
* let dedented = dedent(indented);
|
||||||
|
* assert_eq!(dedented, "line 1\nline 2\n line 3");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* # Notes
|
||||||
|
*
|
||||||
|
* - Empty lines are preserved but have all leading whitespace removed
|
||||||
|
* - Tabs are counted as 4 spaces for indentation purposes
|
||||||
|
*/
|
||||||
|
pub fn dedent(text: &str) -> String {
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
|
||||||
|
// Find the minimum indentation level (ignore empty lines)
|
||||||
|
let min_indent = lines.iter()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.map(|line| {
|
||||||
|
let mut spaces = 0;
|
||||||
|
for c in line.chars() {
|
||||||
|
if c == ' ' {
|
||||||
|
spaces += 1;
|
||||||
|
} else if c == '\t' {
|
||||||
|
spaces += 4; // Count tabs as 4 spaces
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spaces
|
||||||
|
})
|
||||||
|
.min()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Remove that many spaces from the beginning of each line
|
||||||
|
lines.iter()
|
||||||
|
.map(|line| {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
let mut chars = line.chars().peekable();
|
||||||
|
|
||||||
|
// Skip initial spaces up to min_indent
|
||||||
|
while count < min_indent && chars.peek().is_some() {
|
||||||
|
match chars.peek() {
|
||||||
|
Some(' ') => {
|
||||||
|
chars.next();
|
||||||
|
count += 1;
|
||||||
|
},
|
||||||
|
Some('\t') => {
|
||||||
|
chars.next();
|
||||||
|
count += 4;
|
||||||
|
},
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the remaining characters
|
||||||
|
chars.collect::<String>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user