feat: Add support for new OS package
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
- Add a new `sal-os` package containing OS interaction utilities. - Update workspace members to include the new package. - Add README and basic usage examples for the new package.
This commit is contained in:
@@ -40,7 +40,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub mod cmd;
|
||||
pub use sal_mycelium as mycelium;
|
||||
pub mod net;
|
||||
pub mod os;
|
||||
pub use sal_os as os;
|
||||
pub mod postgresclient;
|
||||
pub mod process;
|
||||
pub use sal_redisclient as redisclient;
|
||||
|
245
src/os/README.md
245
src/os/README.md
@@ -1,245 +0,0 @@
|
||||
# SAL OS Module (`sal::os`)
|
||||
|
||||
The `sal::os` module provides a comprehensive suite of operating system interaction utilities. It aims to offer a cross-platform abstraction layer for common OS-level tasks, simplifying system programming in Rust.
|
||||
|
||||
This module is composed of three main sub-modules:
|
||||
- [`fs`](#fs): File system operations.
|
||||
- [`download`](#download): File downloading and basic installation.
|
||||
- [`package`](#package): System package management.
|
||||
|
||||
## Key Design Points
|
||||
|
||||
The `sal::os` module is engineered with several core principles to provide a robust and developer-friendly interface for OS interactions:
|
||||
|
||||
- **Cross-Platform Abstraction**: A primary goal is to offer a unified API for common OS tasks, smoothing over differences between operating systems (primarily Linux and macOS). While it strives for abstraction, it leverages platform-specific tools (e.g., `rsync` on Linux, `robocopy` on Windows for `fs::copy` or `fs::rsync`; `apt` on Debian-based systems, `brew` on macOS for `package` management) for optimal performance and behavior when necessary.
|
||||
- **Modular Structure**: Functionality is organized into logical sub-modules:
|
||||
- `fs`: For comprehensive file and directory manipulation.
|
||||
- `download`: For retrieving files from URLs, with support for extraction and basic installation.
|
||||
- `package`: For interacting with system package managers.
|
||||
- **Granular Error Handling**: Each sub-module features custom error enums (`FsError`, `DownloadError`, `PackageError`) to provide specific and actionable feedback, aiding in debugging and robust error management.
|
||||
- **Sensible Defaults and Defensive Operations**: Many functions are designed to be "defensive," e.g., `mkdir` creates parent directories if they don't exist and doesn't fail if the directory already exists. `delete` doesn't error if the target is already gone.
|
||||
- **Facade for Simplicity**: The `package` sub-module uses a `PackHero` facade to provide a simple entry point for common package operations, automatically detecting the underlying OS and package manager.
|
||||
- **Rhai Scriptability**: A significant portion of the `sal::os` module's functionality is exposed to Rhai scripts via `herodo`, enabling powerful automation of OS-level tasks.
|
||||
|
||||
## `fs` - File System Operations
|
||||
|
||||
The `fs` sub-module (`sal::os::fs`) offers a robust set of functions for interacting with the file system.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
* **Error Handling**: A custom `FsError` enum for detailed error reporting on file system operations.
|
||||
* **File Operations**:
|
||||
* `copy(src, dest)`: Copies files and directories, with support for wildcards and recursive copying. Uses platform-specific commands (`cp -R`, `robocopy /MIR`).
|
||||
* `exist(path)`: Checks if a file or directory exists.
|
||||
* `find_file(dir, filename_pattern)`: Finds a single file in a directory, supporting wildcards.
|
||||
* `find_files(dir, filename_pattern)`: Finds multiple files in a directory, supporting wildcards.
|
||||
* `file_size(path)`: Returns the size of a file in bytes.
|
||||
* `file_read(path)`: Reads the entire content of a file into a string.
|
||||
* `file_write(path, content)`: Writes content to a file, overwriting if it exists, and creating parent directories if needed.
|
||||
* `file_write_append(path, content)`: Appends content to a file, creating it and parent directories if needed.
|
||||
* **Directory Operations**:
|
||||
* `find_dir(parent_dir, dirname_pattern)`: Finds a single directory within a parent directory, supporting wildcards.
|
||||
* `find_dirs(parent_dir, dirname_pattern)`: Finds multiple directories recursively within a parent directory, supporting wildcards.
|
||||
* `delete(path)`: Deletes files or directories.
|
||||
* `mkdir(path)`: Creates a directory, including parent directories if necessary.
|
||||
* `rsync(src, dest)`: Synchronizes directories using platform-specific commands (`rsync -a --delete`, `robocopy /MIR`).
|
||||
* `chdir(path)`: Changes the current working directory.
|
||||
* **Path Operations**:
|
||||
* `mv(src, dest)`: Moves or renames files and directories. Handles cross-device moves by falling back to copy-then-delete.
|
||||
* **Command Utilities**:
|
||||
* `which(command_name)`: Checks if a command exists in the system's PATH and returns its path.
|
||||
|
||||
**Usage Example (fs):**
|
||||
|
||||
```rust
|
||||
use sal::os::fs;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !fs::exist("my_dir") {
|
||||
fs::mkdir("my_dir")?;
|
||||
println!("Created directory 'my_dir'");
|
||||
}
|
||||
|
||||
fs::file_write("my_dir/example.txt", "Hello from SAL!")?;
|
||||
let content = fs::file_read("my_dir/example.txt")?;
|
||||
println!("File content: {}", content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## `download` - File Downloading and Installation
|
||||
|
||||
The `download` sub-module (`sal::os::download`) provides utilities for downloading files from URLs and performing basic installation tasks.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
* **Error Handling**: A custom `DownloadError` enum for download-specific errors.
|
||||
* **File Downloading**:
|
||||
* `download(url, dest_dir, min_size_kb)`: Downloads a file to a specified directory.
|
||||
* Uses `curl` with progress display.
|
||||
* Supports minimum file size checks.
|
||||
* Automatically extracts common archive formats (`.tar.gz`, `.tgz`, `.tar`, `.zip`) into `dest_dir`.
|
||||
* `download_file(url, dest_file_path, min_size_kb)`: Downloads a file to a specific file path without automatic extraction.
|
||||
* **File Permissions**:
|
||||
* `chmod_exec(path)`: Makes a file executable (equivalent to `chmod +x` on Unix-like systems).
|
||||
* **Download and Install**:
|
||||
* `download_install(url, min_size_kb)`: Downloads a file (to `/tmp/`) and attempts to install it if it's a supported package format.
|
||||
* Currently supports `.deb` packages on Debian-based systems.
|
||||
* For `.deb` files, it uses `sudo dpkg --install` and attempts `sudo apt-get install -f -y` to fix dependencies if needed.
|
||||
* Handles archives by extracting them to `/tmp/` first.
|
||||
|
||||
**Usage Example (download):**
|
||||
|
||||
```rust
|
||||
use sal::os::download;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let archive_url = "https://example.com/my_archive.tar.gz";
|
||||
let output_dir = "/tmp/my_app";
|
||||
|
||||
// Download and extract an archive
|
||||
let extracted_path = download::download(archive_url, output_dir, 1024)?; // Min 1MB
|
||||
println!("Archive extracted to: {}", extracted_path);
|
||||
|
||||
// Download a script and make it executable
|
||||
let script_url = "https://example.com/my_script.sh";
|
||||
let script_path = "/tmp/my_script.sh";
|
||||
download::download_file(script_url, script_path, 0)?;
|
||||
download::chmod_exec(script_path)?;
|
||||
println!("Script downloaded and made executable at: {}", script_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## `package` - System Package Management
|
||||
|
||||
The `package` sub-module (`sal::os::package`) offers an abstraction layer for interacting with system package managers like APT (for Debian/Ubuntu) and Homebrew (for macOS).
|
||||
|
||||
**Key Features:**
|
||||
|
||||
* **Error Handling**: A custom `PackageError` enum.
|
||||
* **Platform Detection**: Identifies the current OS (Ubuntu, macOS, or Unknown) to use the appropriate package manager.
|
||||
* **`PackageManager` Trait**: Defines a common interface for package operations:
|
||||
* `install(package_name)`
|
||||
* `remove(package_name)`
|
||||
* `update()` (updates package lists)
|
||||
* `upgrade()` (upgrades all installed packages)
|
||||
* `list_installed()`
|
||||
* `search(query)`
|
||||
* `is_installed(package_name)`
|
||||
* **Implementations**:
|
||||
* `AptPackageManager`: For Debian/Ubuntu systems (uses `apt-get`, `dpkg`).
|
||||
* `BrewPackageManager`: For macOS systems (uses `brew`).
|
||||
* **`PackHero` Facade**: A simple entry point to access package management functions in a platform-agnostic way.
|
||||
* `PackHero::new().install("nginx")?`
|
||||
|
||||
**Usage Example (package):**
|
||||
|
||||
```rust
|
||||
use sal::os::package::PackHero;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let pack_hero = PackHero::new();
|
||||
|
||||
// Check if a package is installed
|
||||
if !pack_hero.is_installed("htop")? {
|
||||
println!("htop is not installed. Attempting to install...");
|
||||
pack_hero.install("htop")?;
|
||||
println!("htop installed successfully.");
|
||||
} else {
|
||||
println!("htop is already installed.");
|
||||
}
|
||||
|
||||
// Update package lists
|
||||
println!("Updating package lists...");
|
||||
pack_hero.update()?;
|
||||
println!("Package lists updated.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Rhai Scripting with `herodo`
|
||||
|
||||
The `sal::os` module is extensively scriptable via `herodo`, allowing for automation of various operating system tasks directly from Rhai scripts. The `sal::rhai::os` module registers the necessary functions.
|
||||
|
||||
### File System (`fs`) Functions
|
||||
|
||||
- `copy(src: String, dest: String) -> String`: Copies files/directories (supports wildcards).
|
||||
- `exist(path: String) -> bool`: Checks if a file or directory exists.
|
||||
- `find_file(dir: String, filename_pattern: String) -> String`: Finds a single file in `dir` matching `filename_pattern`.
|
||||
- `find_files(dir: String, filename_pattern: String) -> Array`: Finds multiple files in `dir` (recursive).
|
||||
- `find_dir(parent_dir: String, dirname_pattern: String) -> String`: Finds a single directory in `parent_dir`.
|
||||
- `find_dirs(parent_dir: String, dirname_pattern: String) -> Array`: Finds multiple directories in `parent_dir` (recursive).
|
||||
- `delete(path: String) -> String`: Deletes a file or directory.
|
||||
- `mkdir(path: String) -> String`: Creates a directory (and parents if needed).
|
||||
- `file_size(path: String) -> Int`: Returns file size in bytes.
|
||||
- `rsync(src: String, dest: String) -> String`: Synchronizes directories.
|
||||
- `chdir(path: String) -> String`: Changes the current working directory.
|
||||
- `file_read(path: String) -> String`: Reads entire file content.
|
||||
- `file_write(path: String, content: String) -> String`: Writes content to a file (overwrites).
|
||||
- `file_write_append(path: String, content: String) -> String`: Appends content to a file.
|
||||
- `mv(src: String, dest: String) -> String`: Moves/renames a file or directory.
|
||||
- `which(command_name: String) -> String`: Checks if a command exists in PATH and returns its path.
|
||||
- `cmd_ensure_exists(commands: String) -> String`: Ensures one or more commands (comma-separated) exist in PATH; throws an error if any are missing.
|
||||
|
||||
### Download Functions
|
||||
|
||||
- `download(url: String, dest_dir: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_dir`, extracts common archives.
|
||||
- `download_file(url: String, dest_file_path: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_file_path` (no extraction).
|
||||
- `download_install(url: String, min_size_kb: Int) -> String`: Downloads and attempts to install (e.g., `.deb` packages).
|
||||
- `chmod_exec(path: String) -> String`: Makes a file executable (`chmod +x`).
|
||||
|
||||
### Package Management Functions
|
||||
|
||||
- `package_install(package_name: String) -> String`: Installs a package.
|
||||
- `package_remove(package_name: String) -> String`: Removes a package.
|
||||
- `package_update() -> String`: Updates package lists.
|
||||
- `package_upgrade() -> String`: Upgrades all installed packages.
|
||||
- `package_list() -> Array`: Lists all installed packages.
|
||||
- `package_search(query: String) -> Array`: Searches for packages.
|
||||
- `package_is_installed(package_name: String) -> bool`: Checks if a package is installed.
|
||||
- `package_set_debug(debug: bool) -> bool`: Enables/disables debug logging for package operations.
|
||||
- `package_platform() -> String`: Returns the detected package platform (e.g., "Ubuntu", "MacOS").
|
||||
|
||||
### Rhai Example
|
||||
|
||||
```rhai
|
||||
// File system operations
|
||||
let test_dir = "/tmp/sal_os_rhai_test";
|
||||
if exist(test_dir) {
|
||||
delete(test_dir);
|
||||
}
|
||||
mkdir(test_dir);
|
||||
print(`Created directory: ${test_dir}`);
|
||||
|
||||
file_write(`${test_dir}/message.txt`, "Hello from Rhai OS module!");
|
||||
let content = file_read(`${test_dir}/message.txt`);
|
||||
print(`File content: ${content}`);
|
||||
|
||||
// Download operation (example URL, may not be active)
|
||||
// let script_url = "https://raw.githubusercontent.com/someuser/somescript/main/script.sh";
|
||||
// let script_path = `${test_dir}/downloaded_script.sh`;
|
||||
// try {
|
||||
// download_file(script_url, script_path, 0);
|
||||
// chmod_exec(script_path);
|
||||
// print(`Downloaded and made executable: ${script_path}`);
|
||||
// } catch (e) {
|
||||
// print(`Download example failed (this is okay for a test): ${e}`);
|
||||
// }
|
||||
|
||||
// Package management (illustrative, requires sudo for install/remove/update)
|
||||
print(`Package platform: ${package_platform()}`);
|
||||
if !package_is_installed("htop") {
|
||||
print("htop is not installed.");
|
||||
// package_install("htop"); // Would require sudo
|
||||
} else {
|
||||
print("htop is already installed.");
|
||||
}
|
||||
|
||||
print("OS module Rhai script finished.");
|
||||
```
|
||||
|
||||
This module provides a powerful and convenient way to handle common OS-level tasks within your Rust applications.
|
@@ -1,522 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
// Define a custom error type for 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.
|
||||
* This function is primarily intended for downloading archives that will be extracted
|
||||
* to a directory.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `url` - The URL to download from
|
||||
* * `dest` - The destination directory where the file will be saved or extracted
|
||||
* * `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
|
||||
*
|
||||
* ```no_run
|
||||
* use sal::os::download;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download("https://example.com/file.zip", "/tmp/", 100)?;
|
||||
*
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* # 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);
|
||||
fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||
|
||||
// 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 full path for the downloaded file
|
||||
let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
|
||||
|
||||
// Create a temporary path for downloading
|
||||
let temp_path = format!("{}.download", file_path);
|
||||
|
||||
// Use curl to download the file with progress bar
|
||||
println!("Downloading {} to {}", url, file_path);
|
||||
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 {
|
||||
// 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, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from URL to a specific file destination using the curl command.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `url` - The URL to download from
|
||||
* * `dest` - The destination file 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
|
||||
* * `Err(DownloadError)` - An error if the download failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```no_run
|
||||
* use sal::os::download_file;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
|
||||
*
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn download_file(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));
|
||||
}
|
||||
}
|
||||
|
||||
// Rename the temporary file to the final destination
|
||||
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
|
||||
Ok(dest.to_string())
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a file executable (equivalent to chmod +x).
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `path` - The path to the file to make executable
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message including the path
|
||||
* * `Err(DownloadError)` - An error if the operation failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```no_run
|
||||
* use sal::os::chmod_exec;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Make a file executable
|
||||
* chmod_exec("/path/to/file")?;
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// Check if the path exists and is a file
|
||||
if !path_obj.exists() {
|
||||
return Err(DownloadError::NotAFile(format!(
|
||||
"Path does not exist: {}",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
if !path_obj.is_file() {
|
||||
return Err(DownloadError::NotAFile(format!(
|
||||
"Path is not a file: {}",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
// Get current permissions
|
||||
let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
// Set executable bit for user, group, and others
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = permissions.mode();
|
||||
// Add executable bit for user, group, and others (equivalent to +x)
|
||||
let new_mode = mode | 0o111;
|
||||
permissions.set_mode(new_mode);
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix platforms, we can't set executable bit directly
|
||||
// Just return success with a warning
|
||||
return Ok(format!(
|
||||
"Made {} executable (note: non-Unix platform, may not be fully supported)",
|
||||
path
|
||||
));
|
||||
}
|
||||
|
||||
// Apply the new permissions
|
||||
fs::set_permissions(path, permissions).map_err(|e| {
|
||||
DownloadError::CommandExecutionFailed(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to set executable permissions: {}", e),
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(format!("Made {} executable", path))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* ```no_run
|
||||
* use sal::os::download_install;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Download and install a .deb package
|
||||
* let result = download_install("https://example.com/package.deb", 100)?;
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* # 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);
|
||||
|
||||
// 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");
|
||||
|
||||
let download_result = if is_archive {
|
||||
// For archives, use the directory-based download function
|
||||
download(url, "/tmp", min_size_kb)?
|
||||
} else {
|
||||
// For regular files, use the file-specific download function
|
||||
download_file(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)
|
||||
}
|
1185
src/os/fs.rs
1185
src/os/fs.rs
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
mod fs;
|
||||
mod download;
|
||||
pub mod package;
|
||||
|
||||
pub use fs::*;
|
||||
pub use download::*;
|
||||
pub use package::*;
|
||||
pub mod platform;
|
@@ -1,964 +0,0 @@
|
||||
use crate::process::CommandResult;
|
||||
use std::process::Command;
|
||||
|
||||
/// Error type for package management operations
|
||||
#[derive(Debug)]
|
||||
pub enum PackageError {
|
||||
/// Command failed with error message
|
||||
CommandFailed(String),
|
||||
/// Command execution failed with IO error
|
||||
CommandExecutionFailed(std::io::Error),
|
||||
/// Unsupported platform
|
||||
UnsupportedPlatform(String),
|
||||
/// Other error
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PackageError::CommandFailed(msg) => write!(f, "Command failed: {}", msg),
|
||||
PackageError::CommandExecutionFailed(e) => write!(f, "Command execution failed: {}", e),
|
||||
PackageError::UnsupportedPlatform(msg) => write!(f, "Unsupported platform: {}", msg),
|
||||
PackageError::Other(msg) => write!(f, "Error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PackageError {}
|
||||
|
||||
/// Platform enum for detecting the current operating system
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Platform {
|
||||
/// Ubuntu Linux
|
||||
Ubuntu,
|
||||
/// macOS
|
||||
MacOS,
|
||||
/// Unknown platform
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
/// Detect the current platform
|
||||
pub fn detect() -> Self {
|
||||
// Check for macOS
|
||||
if std::path::Path::new("/usr/bin/sw_vers").exists() {
|
||||
return Platform::MacOS;
|
||||
}
|
||||
|
||||
// Check for Ubuntu
|
||||
if std::path::Path::new("/etc/lsb-release").exists() {
|
||||
// Read the file to confirm it's Ubuntu
|
||||
if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") {
|
||||
if content.contains("Ubuntu") {
|
||||
return Platform::Ubuntu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Platform::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
// Thread-local storage for debug flag
|
||||
thread_local! {
|
||||
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the debug flag for the current thread
|
||||
pub fn set_thread_local_debug(debug: bool) {
|
||||
DEBUG.with(|cell| {
|
||||
*cell.borrow_mut() = debug;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the debug flag for the current thread
|
||||
pub fn thread_local_debug() -> bool {
|
||||
DEBUG.with(|cell| *cell.borrow())
|
||||
}
|
||||
|
||||
/// Execute a package management command and return the result
|
||||
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
if debug {
|
||||
println!("Executing command: {}", args.join(" "));
|
||||
}
|
||||
|
||||
let output = Command::new(args[0]).args(&args[1..]).output();
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
let result = CommandResult {
|
||||
stdout,
|
||||
stderr,
|
||||
success: output.status.success(),
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
};
|
||||
|
||||
// Always output stdout/stderr when debug is true
|
||||
if debug {
|
||||
if !result.stdout.is_empty() {
|
||||
println!("Command stdout: {}", result.stdout);
|
||||
}
|
||||
|
||||
if !result.stderr.is_empty() {
|
||||
println!("Command stderr: {}", result.stderr);
|
||||
}
|
||||
|
||||
if result.success {
|
||||
println!("Command succeeded with code {}", result.code);
|
||||
} else {
|
||||
println!("Command failed with code {}", result.code);
|
||||
}
|
||||
}
|
||||
|
||||
if result.success {
|
||||
Ok(result)
|
||||
} else {
|
||||
// If command failed and debug is false, output stderr
|
||||
if !debug {
|
||||
println!(
|
||||
"Command failed with code {}: {}",
|
||||
result.code,
|
||||
result.stderr.trim()
|
||||
);
|
||||
}
|
||||
Err(PackageError::CommandFailed(format!(
|
||||
"Command failed with code {}: {}",
|
||||
result.code,
|
||||
result.stderr.trim()
|
||||
)))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Always output error information
|
||||
println!("Command execution failed: {}", e);
|
||||
Err(PackageError::CommandExecutionFailed(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for package managers
|
||||
pub trait PackageManager {
|
||||
/// Install a package
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// Remove a package
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// Update package lists
|
||||
fn update(&self) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// Upgrade installed packages
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// List installed packages
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError>;
|
||||
|
||||
/// Search for packages
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;
|
||||
|
||||
/// Check if a package is installed
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
|
||||
}
|
||||
|
||||
/// APT package manager for Ubuntu
|
||||
pub struct AptPackageManager {
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl AptPackageManager {
|
||||
/// Create a new APT package manager
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for AptPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(
|
||||
&["apt-get", "install", "-y", "--quiet", package],
|
||||
self.debug,
|
||||
)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
|
||||
let packages = result
|
||||
.stdout
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 && parts[1] == "install" {
|
||||
Some(parts[0].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
|
||||
let packages = result
|
||||
.stdout
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
parts[0].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let result = execute_package_command(&["dpkg", "-s", package], self.debug);
|
||||
match result {
|
||||
Ok(cmd_result) => Ok(cmd_result.success),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Homebrew package manager for macOS
|
||||
pub struct BrewPackageManager {
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl BrewPackageManager {
|
||||
/// Create a new Homebrew package manager
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for BrewPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "install", "--quiet", package], self.debug)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "update", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "upgrade", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
|
||||
let packages = result
|
||||
.stdout
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["brew", "search", query], self.debug)?;
|
||||
let packages = result
|
||||
.stdout
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let result = execute_package_command(&["brew", "list", package], self.debug);
|
||||
match result {
|
||||
Ok(cmd_result) => Ok(cmd_result.success),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PackHero factory for package management
|
||||
pub struct PackHero {
|
||||
platform: Platform,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl PackHero {
|
||||
/// Create a new PackHero instance
|
||||
pub fn new() -> Self {
|
||||
let platform = Platform::detect();
|
||||
Self {
|
||||
platform,
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the debug mode
|
||||
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the debug mode
|
||||
pub fn debug(&self) -> bool {
|
||||
self.debug
|
||||
}
|
||||
|
||||
/// Get the detected platform
|
||||
pub fn platform(&self) -> Platform {
|
||||
self.platform
|
||||
}
|
||||
|
||||
/// Get a package manager for the current platform
|
||||
fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
|
||||
match self.platform {
|
||||
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
|
||||
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
|
||||
Platform::Unknown => Err(PackageError::UnsupportedPlatform(
|
||||
"Unsupported platform".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a package
|
||||
pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.install(package)
|
||||
}
|
||||
|
||||
/// Remove a package
|
||||
pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.remove(package)
|
||||
}
|
||||
|
||||
/// Update package lists
|
||||
pub fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.update()
|
||||
}
|
||||
|
||||
/// Upgrade installed packages
|
||||
pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.upgrade()
|
||||
}
|
||||
|
||||
/// List installed packages
|
||||
pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.list_installed()
|
||||
}
|
||||
|
||||
/// Search for packages
|
||||
pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.search(query)
|
||||
}
|
||||
|
||||
/// Check if a package is installed
|
||||
pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.is_installed(package)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Import the std::process::Command directly for some test-specific commands
|
||||
use super::*;
|
||||
use std::process::Command as StdCommand;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
fn test_platform_detection() {
|
||||
// This test will return different results depending on the platform it's run on
|
||||
let platform = Platform::detect();
|
||||
println!("Detected platform: {:?}", platform);
|
||||
|
||||
// Just ensure it doesn't panic
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_debug_flag() {
|
||||
// Test setting and getting the debug flag
|
||||
set_thread_local_debug(true);
|
||||
assert_eq!(thread_local_debug(), true);
|
||||
|
||||
set_thread_local_debug(false);
|
||||
assert_eq!(thread_local_debug(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_package_error_display() {
|
||||
// Test the Display implementation for PackageError
|
||||
let err1 = PackageError::CommandFailed("command failed".to_string());
|
||||
assert_eq!(err1.to_string(), "Command failed: command failed");
|
||||
|
||||
let err2 = PackageError::UnsupportedPlatform("test platform".to_string());
|
||||
assert_eq!(err2.to_string(), "Unsupported platform: test platform");
|
||||
|
||||
let err3 = PackageError::Other("other error".to_string());
|
||||
assert_eq!(err3.to_string(), "Error: other error");
|
||||
|
||||
// We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq
|
||||
}
|
||||
|
||||
// Mock package manager for testing
|
||||
struct MockPackageManager {
|
||||
// debug field is kept for consistency with real package managers
|
||||
#[allow(dead_code)]
|
||||
debug: bool,
|
||||
install_called: Arc<Mutex<bool>>,
|
||||
remove_called: Arc<Mutex<bool>>,
|
||||
update_called: Arc<Mutex<bool>>,
|
||||
upgrade_called: Arc<Mutex<bool>>,
|
||||
list_installed_called: Arc<Mutex<bool>>,
|
||||
search_called: Arc<Mutex<bool>>,
|
||||
is_installed_called: Arc<Mutex<bool>>,
|
||||
// Control what the mock returns
|
||||
should_succeed: bool,
|
||||
}
|
||||
|
||||
impl MockPackageManager {
|
||||
fn new(debug: bool, should_succeed: bool) -> Self {
|
||||
Self {
|
||||
debug,
|
||||
install_called: Arc::new(Mutex::new(false)),
|
||||
remove_called: Arc::new(Mutex::new(false)),
|
||||
update_called: Arc::new(Mutex::new(false)),
|
||||
upgrade_called: Arc::new(Mutex::new(false)),
|
||||
list_installed_called: Arc::new(Mutex::new(false)),
|
||||
search_called: Arc::new(Mutex::new(false)),
|
||||
is_installed_called: Arc::new(Mutex::new(false)),
|
||||
should_succeed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for MockPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
*self.install_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: format!("Installed package {}", package),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed(
|
||||
"Mock install failed".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
*self.remove_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: format!("Removed package {}", package),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed(
|
||||
"Mock remove failed".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
*self.update_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: "Updated package lists".to_string(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed(
|
||||
"Mock update failed".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
*self.upgrade_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: "Upgraded packages".to_string(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed(
|
||||
"Mock upgrade failed".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
*self.list_installed_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(vec!["package1".to_string(), "package2".to_string()])
|
||||
} else {
|
||||
Err(PackageError::CommandFailed(
|
||||
"Mock list_installed failed".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
*self.search_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(vec![
|
||||
format!("result1-{}", query),
|
||||
format!("result2-{}", query),
|
||||
])
|
||||
} else {
|
||||
Err(PackageError::CommandFailed(
|
||||
"Mock search failed".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
*self.is_installed_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(package == "installed-package")
|
||||
} else {
|
||||
Err(PackageError::CommandFailed(
|
||||
"Mock is_installed failed".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom PackHero for testing with a mock package manager
|
||||
struct TestPackHero {
|
||||
platform: Platform,
|
||||
#[allow(dead_code)]
|
||||
debug: bool,
|
||||
mock_manager: MockPackageManager,
|
||||
}
|
||||
|
||||
impl TestPackHero {
|
||||
fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self {
|
||||
Self {
|
||||
platform,
|
||||
debug,
|
||||
mock_manager: MockPackageManager::new(debug, should_succeed),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> {
|
||||
match self.platform {
|
||||
Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager),
|
||||
Platform::Unknown => Err(PackageError::UnsupportedPlatform(
|
||||
"Unsupported platform".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.install(package)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.remove(package)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.update()
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.upgrade()
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.list_installed()
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.search(query)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.is_installed(package)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packhero_with_mock_success() {
|
||||
// Test PackHero with a mock package manager that succeeds
|
||||
let hero = TestPackHero::new(Platform::Ubuntu, false, true);
|
||||
|
||||
// Test install
|
||||
let result = hero.install("test-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.install_called.lock().unwrap());
|
||||
|
||||
// Test remove
|
||||
let result = hero.remove("test-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.remove_called.lock().unwrap());
|
||||
|
||||
// Test update
|
||||
let result = hero.update();
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.update_called.lock().unwrap());
|
||||
|
||||
// Test upgrade
|
||||
let result = hero.upgrade();
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
|
||||
|
||||
// Test list_installed
|
||||
let result = hero.list_installed();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
vec!["package1".to_string(), "package2".to_string()]
|
||||
);
|
||||
assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
|
||||
|
||||
// Test search
|
||||
let result = hero.search("query");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
vec!["result1-query".to_string(), "result2-query".to_string()]
|
||||
);
|
||||
assert!(*hero.mock_manager.search_called.lock().unwrap());
|
||||
|
||||
// Test is_installed
|
||||
let result = hero.is_installed("installed-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
|
||||
|
||||
let result = hero.is_installed("not-installed-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(!result.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packhero_with_mock_failure() {
|
||||
// Test PackHero with a mock package manager that fails
|
||||
let hero = TestPackHero::new(Platform::Ubuntu, false, false);
|
||||
|
||||
// Test install
|
||||
let result = hero.install("test-package");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.install_called.lock().unwrap());
|
||||
|
||||
// Test remove
|
||||
let result = hero.remove("test-package");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.remove_called.lock().unwrap());
|
||||
|
||||
// Test update
|
||||
let result = hero.update();
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.update_called.lock().unwrap());
|
||||
|
||||
// Test upgrade
|
||||
let result = hero.upgrade();
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
|
||||
|
||||
// Test list_installed
|
||||
let result = hero.list_installed();
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
|
||||
|
||||
// Test search
|
||||
let result = hero.search("query");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.search_called.lock().unwrap());
|
||||
|
||||
// Test is_installed
|
||||
let result = hero.is_installed("installed-package");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packhero_unsupported_platform() {
|
||||
// Test PackHero with an unsupported platform
|
||||
let hero = TestPackHero::new(Platform::Unknown, false, true);
|
||||
|
||||
// All operations should fail with UnsupportedPlatform error
|
||||
let result = hero.install("test-package");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(PackageError::UnsupportedPlatform(_)) => (),
|
||||
_ => panic!("Expected UnsupportedPlatform error"),
|
||||
}
|
||||
|
||||
let result = hero.remove("test-package");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(PackageError::UnsupportedPlatform(_)) => (),
|
||||
_ => panic!("Expected UnsupportedPlatform error"),
|
||||
}
|
||||
|
||||
let result = hero.update();
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(PackageError::UnsupportedPlatform(_)) => (),
|
||||
_ => panic!("Expected UnsupportedPlatform error"),
|
||||
}
|
||||
}
|
||||
|
||||
// Real-world tests that actually install and remove packages on Ubuntu
|
||||
// These tests will only run on Ubuntu and will be skipped on other platforms
|
||||
#[test]
|
||||
fn test_real_package_operations_on_ubuntu() {
|
||||
// Check if we're on Ubuntu
|
||||
let platform = Platform::detect();
|
||||
if platform != Platform::Ubuntu {
|
||||
println!(
|
||||
"Skipping real package operations test on non-Ubuntu platform: {:?}",
|
||||
platform
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Running real package operations test on Ubuntu");
|
||||
|
||||
// Create a PackHero instance with debug enabled
|
||||
let mut hero = PackHero::new();
|
||||
hero.set_debug(true);
|
||||
|
||||
// Test package to install/remove
|
||||
let test_package = "wget";
|
||||
|
||||
// First, check if the package is already installed
|
||||
let is_installed_before = match hero.is_installed(test_package) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
println!("Error checking if package is installed: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"Package {} is installed before test: {}",
|
||||
test_package, is_installed_before
|
||||
);
|
||||
|
||||
// If the package is already installed, we'll remove it first
|
||||
if is_installed_before {
|
||||
println!("Removing existing package {} before test", test_package);
|
||||
match hero.remove(test_package) {
|
||||
Ok(_) => println!("Successfully removed package {}", test_package),
|
||||
Err(e) => {
|
||||
println!("Error removing package {}: {}", test_package, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it was removed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
if is_installed {
|
||||
println!("Failed to remove package {}", test_package);
|
||||
return;
|
||||
} else {
|
||||
println!("Verified package {} was removed", test_package);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error checking if package is installed after removal: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now install the package
|
||||
println!("Installing package {}", test_package);
|
||||
match hero.install(test_package) {
|
||||
Ok(_) => println!("Successfully installed package {}", test_package),
|
||||
Err(e) => {
|
||||
println!("Error installing package {}: {}", test_package, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it was installed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
if !is_installed {
|
||||
println!("Failed to install package {}", test_package);
|
||||
return;
|
||||
} else {
|
||||
println!("Verified package {} was installed", test_package);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error checking if package is installed after installation: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the search functionality
|
||||
println!("Searching for packages with 'wget'");
|
||||
match hero.search("wget") {
|
||||
Ok(results) => {
|
||||
println!("Search results: {:?}", results);
|
||||
assert!(
|
||||
results.iter().any(|r| r.contains("wget")),
|
||||
"Search results should contain wget"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error searching for packages: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Test listing installed packages
|
||||
println!("Listing installed packages");
|
||||
match hero.list_installed() {
|
||||
Ok(packages) => {
|
||||
println!("Found {} installed packages", packages.len());
|
||||
// Check if our test package is in the list
|
||||
assert!(
|
||||
packages.iter().any(|p| p == test_package),
|
||||
"Installed packages list should contain {}",
|
||||
test_package
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error listing installed packages: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Now remove the package if it wasn't installed before
|
||||
if !is_installed_before {
|
||||
println!("Removing package {} after test", test_package);
|
||||
match hero.remove(test_package) {
|
||||
Ok(_) => println!("Successfully removed package {}", test_package),
|
||||
Err(e) => {
|
||||
println!("Error removing package {}: {}", test_package, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it was removed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
if is_installed {
|
||||
println!("Failed to remove package {}", test_package);
|
||||
return;
|
||||
} else {
|
||||
println!("Verified package {} was removed", test_package);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error checking if package is installed after removal: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test update functionality
|
||||
println!("Testing package list update");
|
||||
match hero.update() {
|
||||
Ok(_) => println!("Successfully updated package lists"),
|
||||
Err(e) => {
|
||||
println!("Error updating package lists: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
println!("All real package operations tests passed on Ubuntu");
|
||||
}
|
||||
|
||||
// Test to check if apt-get is available on the system
|
||||
#[test]
|
||||
fn test_apt_get_availability() {
|
||||
// This test checks if apt-get is available on the system
|
||||
let output = StdCommand::new("which")
|
||||
.arg("apt-get")
|
||||
.output()
|
||||
.expect("Failed to execute which apt-get");
|
||||
|
||||
let success = output.status.success();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
|
||||
println!("apt-get available: {}", success);
|
||||
if success {
|
||||
println!("apt-get path: {}", stdout.trim());
|
||||
}
|
||||
|
||||
// On Ubuntu, this should pass
|
||||
if Platform::detect() == Platform::Ubuntu {
|
||||
assert!(success, "apt-get should be available on Ubuntu");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,63 +0,0 @@
|
||||
use crate::rhai::error::SalError;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn is_osx() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn is_osx() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn is_linux() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn is_linux() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
pub fn is_arm() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "aarch64"))]
|
||||
pub fn is_arm() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub fn is_x86() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
pub fn is_x86() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn check_linux_x86() -> Result<(), SalError> {
|
||||
if is_linux() && is_x86() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SalError::Generic(
|
||||
"Platform Check Error".to_string(),
|
||||
"This operation is only supported on Linux x86_64.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_macos_arm() -> Result<(), SalError> {
|
||||
if is_osx() && is_arm() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SalError::Generic(
|
||||
"Platform Check Error".to_string(),
|
||||
"This operation is only supported on macOS ARM.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
@@ -2,9 +2,9 @@
|
||||
//!
|
||||
//! This module provides Rhai wrappers for functions that interact with the Rhai engine itself.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, NativeCallContext};
|
||||
use crate::os;
|
||||
use super::error::ToRhaiError;
|
||||
use rhai::{Engine, EvalAltResult, NativeCallContext};
|
||||
use sal_os as os;
|
||||
|
||||
/// Register core module functions with the Rhai engine
|
||||
///
|
||||
@@ -37,7 +37,7 @@ pub fn exec(context: NativeCallContext, source: &str) -> Result<rhai::Dynamic, B
|
||||
let file_name = source.split('/').last().unwrap_or("script.rhai");
|
||||
let dest_path = temp_dir.join(format!("{}-{}", uuid::Uuid::new_v4(), file_name));
|
||||
let dest_str = dest_path.to_str().unwrap();
|
||||
|
||||
|
||||
os::download_file(source, dest_str, 0).to_rhai_error()?;
|
||||
os::file_read(dest_str).to_rhai_error()?
|
||||
} else if os::exist(source) {
|
||||
@@ -50,4 +50,4 @@ pub fn exec(context: NativeCallContext, source: &str) -> Result<rhai::Dynamic, B
|
||||
|
||||
// Execute the script content
|
||||
context.engine().eval(&content)
|
||||
}
|
||||
}
|
||||
|
@@ -7,8 +7,8 @@ mod buildah;
|
||||
mod core;
|
||||
pub mod error;
|
||||
mod nerdctl;
|
||||
mod os;
|
||||
mod platform;
|
||||
// OS module is now provided by sal-os package
|
||||
// Platform module is now provided by sal-os package
|
||||
mod postgresclient;
|
||||
mod process;
|
||||
|
||||
@@ -26,8 +26,8 @@ pub use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
|
||||
// Re-export error module
|
||||
pub use error::*;
|
||||
|
||||
// Re-export specific functions from modules to avoid name conflicts
|
||||
pub use os::{
|
||||
// Re-export specific functions from sal-os package
|
||||
pub use sal_os::rhai::{
|
||||
delete,
|
||||
// Download functions
|
||||
download,
|
||||
@@ -106,7 +106,7 @@ pub use sal_text::rhai::register_text_module;
|
||||
pub use vault::register_crypto_module;
|
||||
|
||||
// Rename copy functions to avoid conflicts
|
||||
pub use os::copy as os_copy;
|
||||
pub use sal_os::rhai::copy as os_copy;
|
||||
|
||||
/// Register all SAL modules with the Rhai engine
|
||||
///
|
||||
@@ -132,7 +132,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
core::register_core_module(engine)?;
|
||||
|
||||
// Register OS module functions
|
||||
os::register_os_module(engine)?;
|
||||
sal_os::rhai::register_os_module(engine)?;
|
||||
|
||||
// Register Process module functions
|
||||
process::register_process_module(engine)?;
|
||||
@@ -167,8 +167,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
// Register PostgreSQL client module functions
|
||||
postgresclient::register_postgresclient_module(engine)?;
|
||||
|
||||
// Register Platform module functions
|
||||
platform::register(engine);
|
||||
// Platform functions are now registered by sal-os package
|
||||
|
||||
// Register Screen module functions
|
||||
screen::register(engine);
|
||||
|
356
src/rhai/os.rs
356
src/rhai/os.rs
@@ -1,356 +0,0 @@
|
||||
//! Rhai wrappers for OS module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the OS module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array};
|
||||
use crate::os;
|
||||
use crate::os::package::PackHero;
|
||||
use super::error::{ToRhaiError, register_error_types};
|
||||
|
||||
/// Register OS module functions with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the functions with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||
pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register error types
|
||||
register_error_types(engine)?;
|
||||
|
||||
// Register file system functions
|
||||
engine.register_fn("copy", copy);
|
||||
engine.register_fn("copy_bin", copy_bin);
|
||||
engine.register_fn("exist", exist);
|
||||
engine.register_fn("find_file", find_file);
|
||||
engine.register_fn("find_files", find_files);
|
||||
engine.register_fn("find_dir", find_dir);
|
||||
engine.register_fn("find_dirs", find_dirs);
|
||||
engine.register_fn("delete", delete);
|
||||
engine.register_fn("mkdir", mkdir);
|
||||
engine.register_fn("file_size", file_size);
|
||||
engine.register_fn("rsync", rsync);
|
||||
engine.register_fn("chdir", chdir);
|
||||
engine.register_fn("file_read", file_read);
|
||||
engine.register_fn("file_write", file_write);
|
||||
engine.register_fn("file_write_append", file_write_append);
|
||||
|
||||
// Register command check functions
|
||||
engine.register_fn("which", which);
|
||||
engine.register_fn("cmd_ensure_exists", cmd_ensure_exists);
|
||||
|
||||
// Register download functions
|
||||
engine.register_fn("download", download);
|
||||
engine.register_fn("download_file", download_file);
|
||||
engine.register_fn("download_install", download_install);
|
||||
engine.register_fn("chmod_exec", chmod_exec);
|
||||
|
||||
// Register move function
|
||||
engine.register_fn("mv", mv);
|
||||
|
||||
// Register package management functions
|
||||
engine.register_fn("package_install", package_install);
|
||||
engine.register_fn("package_remove", package_remove);
|
||||
engine.register_fn("package_update", package_update);
|
||||
engine.register_fn("package_upgrade", package_upgrade);
|
||||
engine.register_fn("package_list", package_list);
|
||||
engine.register_fn("package_search", package_search);
|
||||
engine.register_fn("package_is_installed", package_is_installed);
|
||||
engine.register_fn("package_set_debug", package_set_debug);
|
||||
engine.register_fn("package_platform", package_platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//
|
||||
// File System Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for os::copy
|
||||
///
|
||||
/// Recursively copy a file or directory from source to destination.
|
||||
pub fn copy(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::copy(src, dest).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::copy_bin
|
||||
///
|
||||
/// Copy a binary to the correct location based on OS and user privileges.
|
||||
pub fn copy_bin(src: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::copy_bin(src).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::exist
|
||||
///
|
||||
/// Check if a file or directory exists.
|
||||
pub fn exist(path: &str) -> bool {
|
||||
os::exist(path)
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_file
|
||||
///
|
||||
/// Find a file in a directory (with support for wildcards).
|
||||
pub fn find_file(dir: &str, filename: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::find_file(dir, filename).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_files
|
||||
///
|
||||
/// Find multiple files in a directory (recursive, with support for wildcards).
|
||||
pub fn find_files(dir: &str, filename: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let files = os::find_files(dir, filename).to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for file in files {
|
||||
array.push(file.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_dir
|
||||
///
|
||||
/// Find a directory in a parent directory (with support for wildcards).
|
||||
pub fn find_dir(dir: &str, dirname: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::find_dir(dir, dirname).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_dirs
|
||||
///
|
||||
/// Find multiple directories in a parent directory (recursive, with support for wildcards).
|
||||
pub fn find_dirs(dir: &str, dirname: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let dirs = os::find_dirs(dir, dirname).to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for dir in dirs {
|
||||
array.push(dir.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::delete
|
||||
///
|
||||
/// Delete a file or directory (defensive - doesn't error if file doesn't exist).
|
||||
pub fn delete(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::delete(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::mkdir
|
||||
///
|
||||
/// Create a directory and all parent directories (defensive - doesn't error if directory exists).
|
||||
pub fn mkdir(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::mkdir(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_size
|
||||
///
|
||||
/// Get the size of a file in bytes.
|
||||
pub fn file_size(path: &str) -> Result<i64, Box<EvalAltResult>> {
|
||||
os::file_size(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::rsync
|
||||
///
|
||||
/// Sync directories using rsync (or platform equivalent).
|
||||
pub fn rsync(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::rsync(src, dest).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::chdir
|
||||
///
|
||||
/// Change the current working directory.
|
||||
pub fn chdir(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::chdir(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_read
|
||||
///
|
||||
/// Read the contents of a file.
|
||||
pub fn file_read(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::file_read(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_write
|
||||
///
|
||||
/// Write content to a file (creates the file if it doesn't exist, overwrites if it does).
|
||||
pub fn file_write(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::file_write(path, content).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_write_append
|
||||
///
|
||||
/// Append content to a file (creates the file if it doesn't exist).
|
||||
pub fn file_write_append(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::file_write_append(path, content).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::mv
|
||||
///
|
||||
/// Move a file or directory from source to destination.
|
||||
pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::mv(src, dest).to_rhai_error()
|
||||
}
|
||||
|
||||
//
|
||||
// Download Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for os::download
|
||||
///
|
||||
/// Download a file from URL to destination using the curl command.
|
||||
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
|
||||
os::download(url, dest, min_size_kb).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::download_file
|
||||
///
|
||||
/// Download a file from URL to a specific file destination using the curl command.
|
||||
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
|
||||
os::download_file(url, dest, min_size_kb).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::download_install
|
||||
///
|
||||
/// Download a file and install it if it's a supported package format.
|
||||
pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
|
||||
os::download_install(url, min_size_kb).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::chmod_exec
|
||||
///
|
||||
/// Make a file executable (equivalent to chmod +x).
|
||||
pub fn chmod_exec(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::chmod_exec(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::which
|
||||
///
|
||||
/// Check if a command exists in the system PATH.
|
||||
pub fn which(command: &str) -> String {
|
||||
os::which(command)
|
||||
}
|
||||
|
||||
/// Wrapper for os::cmd_ensure_exists
|
||||
///
|
||||
/// Ensure that one or more commands exist in the system PATH.
|
||||
/// If any command doesn't exist, an error is thrown.
|
||||
pub fn cmd_ensure_exists(commands: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::cmd_ensure_exists(commands).to_rhai_error()
|
||||
}
|
||||
|
||||
//
|
||||
// Package Management Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for os::package::PackHero::install
|
||||
///
|
||||
/// Install a package using the system package manager.
|
||||
pub fn package_install(package: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.install(package)
|
||||
.map(|_| format!("Package '{}' installed successfully", package))
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::remove
|
||||
///
|
||||
/// Remove a package using the system package manager.
|
||||
pub fn package_remove(package: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.remove(package)
|
||||
.map(|_| format!("Package '{}' removed successfully", package))
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::update
|
||||
///
|
||||
/// Update package lists using the system package manager.
|
||||
pub fn package_update() -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.update()
|
||||
.map(|_| "Package lists updated successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::upgrade
|
||||
///
|
||||
/// Upgrade installed packages using the system package manager.
|
||||
pub fn package_upgrade() -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.upgrade()
|
||||
.map(|_| "Packages upgraded successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::list_installed
|
||||
///
|
||||
/// List installed packages using the system package manager.
|
||||
pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
let packages = hero.list_installed().to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for package in packages {
|
||||
array.push(package.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::search
|
||||
///
|
||||
/// Search for packages using the system package manager.
|
||||
pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
let packages = hero.search(query).to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for package in packages {
|
||||
array.push(package.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::is_installed
|
||||
///
|
||||
/// Check if a package is installed using the system package manager.
|
||||
pub fn package_is_installed(package: &str) -> Result<bool, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.is_installed(package).to_rhai_error()
|
||||
}
|
||||
|
||||
// Thread-local storage for package debug flag
|
||||
thread_local! {
|
||||
static PACKAGE_DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the debug mode for package management operations
|
||||
pub fn package_set_debug(debug: bool) -> bool {
|
||||
let mut hero = PackHero::new();
|
||||
hero.set_debug(debug);
|
||||
|
||||
// Also set the thread-local debug flag
|
||||
PACKAGE_DEBUG.with(|cell| {
|
||||
*cell.borrow_mut() = debug;
|
||||
});
|
||||
|
||||
debug
|
||||
}
|
||||
|
||||
/// Get the current platform name for package management
|
||||
pub fn package_platform() -> String {
|
||||
let hero = PackHero::new();
|
||||
match hero.platform() {
|
||||
os::package::Platform::Ubuntu => "Ubuntu".to_string(),
|
||||
os::package::Platform::MacOS => "MacOS".to_string(),
|
||||
os::package::Platform::Unknown => "Unknown".to_string(),
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
use crate::os::platform;
|
||||
use rhai::{plugin::*, Engine};
|
||||
|
||||
#[export_module]
|
||||
pub mod platform_functions {
|
||||
#[rhai_fn(name = "platform_is_osx")]
|
||||
pub fn is_osx() -> bool {
|
||||
platform::is_osx()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "platform_is_linux")]
|
||||
pub fn is_linux() -> bool {
|
||||
platform::is_linux()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "platform_is_arm")]
|
||||
pub fn is_arm() -> bool {
|
||||
platform::is_arm()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "platform_is_x86")]
|
||||
pub fn is_x86() -> bool {
|
||||
platform::is_x86()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "platform_check_linux_x86")]
|
||||
pub fn check_linux_x86() -> Result<(), crate::rhai::error::SalError> {
|
||||
platform::check_linux_x86()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "platform_check_macos_arm")]
|
||||
pub fn check_macos_arm() -> Result<(), crate::rhai::error::SalError> {
|
||||
platform::check_macos_arm()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(engine: &mut Engine) {
|
||||
let platform_module = exported_module!(platform_functions);
|
||||
engine.register_global_module(platform_module.into());
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
// File: /root/code/git.threefold.info/herocode/sal/src/virt/nerdctl/container.rs
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
|
||||
use crate::os;
|
||||
use super::container_types::Container;
|
||||
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
|
||||
use sal_os as os;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl Container {
|
||||
/// Create a new container reference with the given name
|
||||
@@ -18,18 +18,22 @@ impl Container {
|
||||
pub fn new(name: &str) -> Result<Self, NerdctlError> {
|
||||
// Check if required commands exist
|
||||
match os::cmd_ensure_exists("nerdctl,runc,buildah") {
|
||||
Err(e) => return Err(NerdctlError::CommandExecutionFailed(
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound,
|
||||
format!("Required commands not found: {}", e))
|
||||
)),
|
||||
Err(e) => {
|
||||
return Err(NerdctlError::CommandExecutionFailed(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Required commands not found: {}", e),
|
||||
)))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
// Check if container exists
|
||||
let result = execute_nerdctl_command(&["ps", "-a", "--format", "{{.Names}} {{.ID}}"])?;
|
||||
|
||||
|
||||
// Look for the container name in the output
|
||||
let container_id = result.stdout.lines()
|
||||
let container_id = result
|
||||
.stdout
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if line.starts_with(&format!("{} ", name)) {
|
||||
Some(line.split_whitespace().nth(1)?.to_string())
|
||||
@@ -38,7 +42,7 @@ impl Container {
|
||||
}
|
||||
})
|
||||
.next();
|
||||
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
container_id,
|
||||
@@ -59,7 +63,7 @@ impl Container {
|
||||
snapshotter: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Create a container from an image
|
||||
///
|
||||
/// # Arguments
|
||||
|
Reference in New Issue
Block a user