...
This commit is contained in:
parent
0fa9eddd1c
commit
21893ce225
@ -26,6 +26,17 @@ cp target/debug/herodo ~/hero/bin/herodo
|
||||
# Check if a script name was provided
|
||||
if [ $# -eq 1 ]; then
|
||||
echo "Running specified test: $1"
|
||||
herodo "src/herodo/scripts/$1.rhai"
|
||||
|
||||
# Check if the script exists in src/rhaiexamples/
|
||||
if [ -f "src/rhaiexamples/$1.rhai" ]; then
|
||||
herodo "src/rhaiexamples/$1.rhai"
|
||||
# Check if the script exists in src/herodo/scripts/
|
||||
elif [ -f "src/herodo/scripts/$1.rhai" ]; then
|
||||
herodo "src/herodo/scripts/$1.rhai"
|
||||
else
|
||||
echo "Error: Script $1.rhai not found in src/rhaiexamples/ or src/herodo/scripts/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
100
examples/package_test.rs
Normal file
100
examples/package_test.rs
Normal file
@ -0,0 +1,100 @@
|
||||
//! Example of using the package management module
|
||||
//!
|
||||
//! This example demonstrates how to use the package management module
|
||||
//! to install, remove, and manage packages on different platforms.
|
||||
|
||||
use sal::os::package::{PackHero, Platform};
|
||||
|
||||
fn main() {
|
||||
// Create a new PackHero instance
|
||||
let mut hero = PackHero::new();
|
||||
|
||||
// Enable debug output
|
||||
hero.set_debug(true);
|
||||
|
||||
// Detect the platform
|
||||
let platform = hero.platform();
|
||||
println!("Detected platform: {:?}", platform);
|
||||
|
||||
// Only proceed if we're on a supported platform
|
||||
if platform == Platform::Unknown {
|
||||
println!("Unsupported platform. This example only works on Ubuntu and macOS.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test package to install/check
|
||||
let test_package = if platform == Platform::Ubuntu { "wget" } else { "wget" };
|
||||
|
||||
// Check if the package is installed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
println!("Package {} is installed: {}", test_package, is_installed);
|
||||
|
||||
if is_installed {
|
||||
println!("Package {} is already installed", test_package);
|
||||
} else {
|
||||
println!("Package {} is not installed, attempting to install...", test_package);
|
||||
|
||||
// Try to install the package
|
||||
match hero.install(test_package) {
|
||||
Ok(_) => println!("Successfully installed package {}", test_package),
|
||||
Err(e) => println!("Failed to install package {}: {}", test_package, e),
|
||||
}
|
||||
|
||||
// Check if it was installed successfully
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed_now) => {
|
||||
if is_installed_now {
|
||||
println!("Verified package {} was installed successfully", test_package);
|
||||
} else {
|
||||
println!("Package {} was not installed successfully", test_package);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error checking if package is installed: {}", e),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error checking if package is installed: {}", e),
|
||||
}
|
||||
|
||||
// Search for packages
|
||||
let search_term = "wget";
|
||||
println!("Searching for packages with term '{}'...", search_term);
|
||||
match hero.search(search_term) {
|
||||
Ok(results) => {
|
||||
println!("Found {} packages matching '{}'", results.len(), search_term);
|
||||
for (i, package) in results.iter().enumerate().take(5) {
|
||||
println!(" {}. {}", i + 1, package);
|
||||
}
|
||||
if results.len() > 5 {
|
||||
println!(" ... and {} more", results.len() - 5);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error searching for packages: {}", e),
|
||||
}
|
||||
|
||||
// List installed packages
|
||||
println!("Listing installed packages...");
|
||||
match hero.list_installed() {
|
||||
Ok(packages) => {
|
||||
println!("Found {} installed packages", packages.len());
|
||||
println!("First 5 installed packages:");
|
||||
for (i, package) in packages.iter().enumerate().take(5) {
|
||||
println!(" {}. {}", i + 1, package);
|
||||
}
|
||||
if packages.len() > 5 {
|
||||
println!(" ... and {} more", packages.len() - 5);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error listing installed packages: {}", e),
|
||||
}
|
||||
|
||||
// Update package lists
|
||||
println!("Updating package lists...");
|
||||
match hero.update() {
|
||||
Ok(_) => println!("Successfully updated package lists"),
|
||||
Err(e) => println!("Error updating package lists: {}", e),
|
||||
}
|
||||
|
||||
println!("Package management example completed");
|
||||
}
|
474
rfs_implementation_plan.md
Normal file
474
rfs_implementation_plan.md
Normal file
@ -0,0 +1,474 @@
|
||||
# RFS Wrapper Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
We'll create a Rust wrapper for the RFS (Remote File System) tool that follows the builder pattern, similar to the existing implementations for buildah and nerdctl in the codebase. This wrapper will provide a fluent API for mounting, unmounting, listing mounts, configuring mount options, and packing directories into filesystem layers.
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
src/virt/rfs/
|
||||
├── mod.rs # Module exports and common types
|
||||
├── cmd.rs # Command execution functions
|
||||
├── mount.rs # Mount operations
|
||||
├── pack.rs # Packing operations
|
||||
├── builder.rs # Builder pattern implementation
|
||||
├── types.rs # Type definitions
|
||||
└── error.rs # Error handling
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Error Handling
|
||||
|
||||
```rust
|
||||
// error.rs
|
||||
#[derive(Debug)]
|
||||
pub enum RfsError {
|
||||
CommandFailed(String),
|
||||
InvalidArgument(String),
|
||||
MountFailed(String),
|
||||
UnmountFailed(String),
|
||||
ListFailed(String),
|
||||
PackFailed(String),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RfsError {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
impl std::error::Error for RfsError {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Command Execution
|
||||
|
||||
```rust
|
||||
// cmd.rs
|
||||
use crate::process::{run_command, CommandResult};
|
||||
use super::error::RfsError;
|
||||
|
||||
pub fn execute_rfs_command(args: &[&str]) -> Result<CommandResult, RfsError> {
|
||||
// Implementation similar to buildah and nerdctl
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Types
|
||||
|
||||
```rust
|
||||
// types.rs
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mount {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
pub fs_type: String,
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MountType {
|
||||
Local,
|
||||
SSH,
|
||||
S3,
|
||||
WebDAV,
|
||||
// Other mount types
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StoreSpec {
|
||||
pub spec_type: String,
|
||||
pub options: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl StoreSpec {
|
||||
pub fn new(spec_type: &str) -> Self {
|
||||
Self {
|
||||
spec_type: spec_type.to_string(),
|
||||
options: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_option(mut self, key: &str, value: &str) -> Self {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
let mut result = self.spec_type.clone();
|
||||
|
||||
if !self.options.is_empty() {
|
||||
result.push_str(":");
|
||||
let options: Vec<String> = self.options
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}={}", k, v))
|
||||
.collect();
|
||||
result.push_str(&options.join(","));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Builder Pattern
|
||||
|
||||
```rust
|
||||
// builder.rs
|
||||
use std::collections::HashMap;
|
||||
use super::{Mount, MountType, RfsError, execute_rfs_command, StoreSpec};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RfsBuilder {
|
||||
source: String,
|
||||
target: String,
|
||||
mount_type: MountType,
|
||||
options: HashMap<String, String>,
|
||||
mount_id: Option<String>,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl RfsBuilder {
|
||||
pub fn new(source: &str, target: &str, mount_type: MountType) -> Self {
|
||||
Self {
|
||||
source: source.to_string(),
|
||||
target: target.to_string(),
|
||||
mount_type,
|
||||
options: HashMap::new(),
|
||||
mount_id: None,
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_option(mut self, key: &str, value: &str) -> Self {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_options(mut self, options: HashMap<&str, &str>) -> Self {
|
||||
for (key, value) in options {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mount(self) -> Result<Mount, RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
pub fn unmount(&self) -> Result<(), RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Other methods
|
||||
}
|
||||
|
||||
// Packing functionality
|
||||
pub struct PackBuilder {
|
||||
directory: String,
|
||||
output: String,
|
||||
store_specs: Vec<StoreSpec>,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl PackBuilder {
|
||||
pub fn new(directory: &str, output: &str) -> Self {
|
||||
Self {
|
||||
directory: directory.to_string(),
|
||||
output: output.to_string(),
|
||||
store_specs: Vec::new(),
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_store_spec(mut self, store_spec: StoreSpec) -> Self {
|
||||
self.store_specs.push(store_spec);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_store_specs(mut self, store_specs: Vec<StoreSpec>) -> Self {
|
||||
self.store_specs.extend(store_specs);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pack(self) -> Result<(), RfsError> {
|
||||
// Implementation for packing a directory into a filesystem layer
|
||||
let mut args = vec!["pack"];
|
||||
|
||||
// Add output file
|
||||
args.push("-m");
|
||||
args.push(&self.output);
|
||||
|
||||
// Add store specs
|
||||
if !self.store_specs.is_empty() {
|
||||
args.push("-s");
|
||||
let specs: Vec<String> = self.store_specs
|
||||
.iter()
|
||||
.map(|spec| spec.to_string())
|
||||
.collect();
|
||||
args.push(&specs.join(","));
|
||||
}
|
||||
|
||||
// Add directory
|
||||
args.push(&self.directory);
|
||||
|
||||
// Convert to string slices for the command
|
||||
let args_str: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
// Execute the command
|
||||
let result = execute_rfs_command(&args_str)?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::PackFailed(result.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Mount Operations
|
||||
|
||||
```rust
|
||||
// mount.rs
|
||||
use super::{RfsBuilder, Mount, RfsError, execute_rfs_command};
|
||||
|
||||
pub fn list_mounts() -> Result<Vec<Mount>, RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
pub fn unmount_all() -> Result<(), RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Other mount-related functions
|
||||
```
|
||||
|
||||
### 6. Pack Operations
|
||||
|
||||
```rust
|
||||
// pack.rs
|
||||
use super::{PackBuilder, StoreSpec, RfsError, execute_rfs_command};
|
||||
|
||||
pub fn pack_directory(directory: &str, output: &str, store_specs: &[StoreSpec]) -> Result<(), RfsError> {
|
||||
PackBuilder::new(directory, output)
|
||||
.with_store_specs(store_specs.to_vec())
|
||||
.pack()
|
||||
}
|
||||
|
||||
// Other pack-related functions
|
||||
```
|
||||
|
||||
### 7. Module Exports
|
||||
|
||||
```rust
|
||||
// mod.rs
|
||||
mod cmd;
|
||||
mod error;
|
||||
mod mount;
|
||||
mod pack;
|
||||
mod builder;
|
||||
mod types;
|
||||
|
||||
pub use error::RfsError;
|
||||
pub use builder::{RfsBuilder, PackBuilder};
|
||||
pub use types::{Mount, MountType, StoreSpec};
|
||||
pub use mount::{list_mounts, unmount_all};
|
||||
pub use pack::pack_directory;
|
||||
|
||||
// Re-export the execute_rfs_command function for use in other modules
|
||||
pub(crate) use cmd::execute_rfs_command;
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Mounting Example
|
||||
|
||||
```rust
|
||||
use crate::virt::rfs::{RfsBuilder, MountType};
|
||||
|
||||
// Create a new RFS mount with builder pattern
|
||||
let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH)
|
||||
.with_option("port", "2222")
|
||||
.with_option("identity_file", "/path/to/key")
|
||||
.with_debug(true)
|
||||
.mount()?;
|
||||
|
||||
// List all mounts
|
||||
let mounts = list_mounts()?;
|
||||
for mount in mounts {
|
||||
println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target);
|
||||
}
|
||||
|
||||
// Unmount
|
||||
mount.unmount()?;
|
||||
```
|
||||
|
||||
### Packing Example
|
||||
|
||||
```rust
|
||||
use crate::virt::rfs::{PackBuilder, StoreSpec};
|
||||
|
||||
// Create store specifications
|
||||
let store_spec1 = StoreSpec::new("file")
|
||||
.with_option("path", "/path/to/store");
|
||||
|
||||
let store_spec2 = StoreSpec::new("s3")
|
||||
.with_option("bucket", "my-bucket")
|
||||
.with_option("region", "us-east-1");
|
||||
|
||||
// Pack a directory with builder pattern
|
||||
let result = PackBuilder::new("/path/to/directory", "output.fl")
|
||||
.with_store_spec(store_spec1)
|
||||
.with_store_spec(store_spec2)
|
||||
.with_debug(true)
|
||||
.pack()?;
|
||||
|
||||
// Or use the convenience function
|
||||
pack_directory("/path/to/directory", "output.fl", &[store_spec1, store_spec2])?;
|
||||
```
|
||||
|
||||
## Rhai Integration
|
||||
|
||||
We'll also need to create a Rhai module to expose the RFS functionality to Rhai scripts:
|
||||
|
||||
```rust
|
||||
// src/rhai/rfs.rs
|
||||
use rhai::{Engine, EvalAltResult, RegisterFn};
|
||||
use crate::virt::rfs::{RfsBuilder, MountType, list_mounts, unmount_all, PackBuilder, StoreSpec};
|
||||
|
||||
pub fn register(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register RFS functions
|
||||
engine.register_fn("rfs_mount", rfs_mount);
|
||||
engine.register_fn("rfs_unmount", rfs_unmount);
|
||||
engine.register_fn("rfs_list_mounts", rfs_list_mounts);
|
||||
engine.register_fn("rfs_unmount_all", rfs_unmount_all);
|
||||
engine.register_fn("rfs_pack", rfs_pack);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Function implementations
|
||||
fn rfs_mount(source: &str, target: &str, mount_type: &str, options_map: rhai::Map) -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_unmount(target: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_list_mounts() -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_unmount_all() -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_pack(directory: &str, output: &str, store_specs: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Flow
|
||||
|
||||
Here's a diagram showing the flow of the implementation:
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class RfsBuilder {
|
||||
+String source
|
||||
+String target
|
||||
+MountType mount_type
|
||||
+HashMap options
|
||||
+Option~String~ mount_id
|
||||
+bool debug
|
||||
+new(source, target, mount_type)
|
||||
+with_option(key, value)
|
||||
+with_options(options)
|
||||
+with_debug(debug)
|
||||
+mount()
|
||||
+unmount()
|
||||
}
|
||||
|
||||
class PackBuilder {
|
||||
+String directory
|
||||
+String output
|
||||
+Vec~StoreSpec~ store_specs
|
||||
+bool debug
|
||||
+new(directory, output)
|
||||
+with_store_spec(store_spec)
|
||||
+with_store_specs(store_specs)
|
||||
+with_debug(debug)
|
||||
+pack()
|
||||
}
|
||||
|
||||
class Mount {
|
||||
+String id
|
||||
+String source
|
||||
+String target
|
||||
+String fs_type
|
||||
+Vec~String~ options
|
||||
}
|
||||
|
||||
class MountType {
|
||||
<<enumeration>>
|
||||
Local
|
||||
SSH
|
||||
S3
|
||||
WebDAV
|
||||
}
|
||||
|
||||
class StoreSpec {
|
||||
+String spec_type
|
||||
+HashMap options
|
||||
+new(spec_type)
|
||||
+with_option(key, value)
|
||||
+to_string()
|
||||
}
|
||||
|
||||
class RfsError {
|
||||
<<enumeration>>
|
||||
CommandFailed
|
||||
InvalidArgument
|
||||
MountFailed
|
||||
UnmountFailed
|
||||
ListFailed
|
||||
PackFailed
|
||||
Other
|
||||
}
|
||||
|
||||
RfsBuilder --> Mount : creates
|
||||
RfsBuilder --> RfsError : may throw
|
||||
RfsBuilder --> MountType : uses
|
||||
PackBuilder --> RfsError : may throw
|
||||
PackBuilder --> StoreSpec : uses
|
||||
Mount --> RfsError : may throw
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create the directory structure for the RFS module
|
||||
2. Implement the error handling module
|
||||
3. Implement the command execution module
|
||||
4. Define the types for mounts, mount operations, and store specifications
|
||||
5. Implement the builder pattern for RFS operations (mount and pack)
|
||||
6. Implement the mount operations
|
||||
7. Implement the pack operations
|
||||
8. Create the module exports
|
||||
9. Add Rhai integration
|
||||
10. Write tests for the implementation
|
||||
11. Update documentation
|
154
src/docs/docs/sal/rfs.md
Normal file
154
src/docs/docs/sal/rfs.md
Normal file
@ -0,0 +1,154 @@
|
||||
# RFS (Remote File System)
|
||||
|
||||
The RFS module provides a Rust wrapper for the RFS tool, which allows mounting remote filesystems locally and managing filesystem layers.
|
||||
|
||||
## Overview
|
||||
|
||||
RFS (Remote File System) is a tool that enables mounting various types of remote filesystems locally, as well as creating and managing filesystem layers. The SAL library provides a Rust wrapper for RFS with a fluent builder API, making it easy to use in your applications.
|
||||
|
||||
## Features
|
||||
|
||||
- Mount remote filesystems locally (SSH, S3, WebDAV, etc.)
|
||||
- List mounted filesystems
|
||||
- Unmount filesystems
|
||||
- Pack directories into filesystem layers
|
||||
- Unpack filesystem layers
|
||||
- List contents of filesystem layers
|
||||
- Verify filesystem layers
|
||||
|
||||
## Usage in Rust
|
||||
|
||||
### Mounting a Filesystem
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::{RfsBuilder, MountType};
|
||||
|
||||
// Create a new RFS builder
|
||||
let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH)
|
||||
.with_option("port", "2222")
|
||||
.with_option("identity_file", "/path/to/key")
|
||||
.with_debug(true)
|
||||
.mount()?;
|
||||
|
||||
println!("Mounted filesystem with ID: {}", mount.id);
|
||||
```
|
||||
|
||||
### Listing Mounts
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::list_mounts;
|
||||
|
||||
// List all mounts
|
||||
let mounts = list_mounts()?;
|
||||
for mount in mounts {
|
||||
println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target);
|
||||
}
|
||||
```
|
||||
|
||||
### Unmounting a Filesystem
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::unmount;
|
||||
|
||||
// Unmount a filesystem
|
||||
unmount("/local/mount/point")?;
|
||||
```
|
||||
|
||||
### Packing a Directory
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::{PackBuilder, StoreSpec};
|
||||
|
||||
// Create store specifications
|
||||
let store_spec = StoreSpec::new("file")
|
||||
.with_option("path", "/path/to/store");
|
||||
|
||||
// Pack a directory with builder pattern
|
||||
let result = PackBuilder::new("/path/to/directory", "output.fl")
|
||||
.with_store_spec(store_spec)
|
||||
.with_debug(true)
|
||||
.pack()?;
|
||||
```
|
||||
|
||||
### Unpacking a Filesystem Layer
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::unpack;
|
||||
|
||||
// Unpack a filesystem layer
|
||||
unpack("input.fl", "/path/to/unpack")?;
|
||||
```
|
||||
|
||||
## Usage in Rhai Scripts
|
||||
|
||||
### Mounting a Filesystem
|
||||
|
||||
```rhai
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"port": "22",
|
||||
"identity_file": "/path/to/key",
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
```
|
||||
|
||||
### Listing Mounts
|
||||
|
||||
```rhai
|
||||
// List all mounts
|
||||
let mounts = rfs_list_mounts();
|
||||
print(`Number of mounts: ${mounts.len()}`);
|
||||
|
||||
for mount in mounts {
|
||||
print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Unmounting a Filesystem
|
||||
|
||||
```rhai
|
||||
// Unmount the directory
|
||||
rfs_unmount("/local/mount/point");
|
||||
```
|
||||
|
||||
### Packing a Directory
|
||||
|
||||
```rhai
|
||||
// Pack the directory
|
||||
// Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket"
|
||||
rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store");
|
||||
```
|
||||
|
||||
### Unpacking a Filesystem Layer
|
||||
|
||||
```rhai
|
||||
// Unpack the filesystem layer
|
||||
rfs_unpack("output.fl", "/path/to/unpack");
|
||||
```
|
||||
|
||||
## Mount Types
|
||||
|
||||
The RFS module supports various mount types:
|
||||
|
||||
- **Local**: Mount a local directory
|
||||
- **SSH**: Mount a remote directory via SSH
|
||||
- **S3**: Mount an S3 bucket
|
||||
- **WebDAV**: Mount a WebDAV server
|
||||
|
||||
## Store Specifications
|
||||
|
||||
When packing a directory into a filesystem layer, you can specify one or more stores to use. Each store has a type and options:
|
||||
|
||||
- **File**: Store files on the local filesystem
|
||||
- Options: `path` (path to the store)
|
||||
- **S3**: Store files in an S3 bucket
|
||||
- Options: `bucket` (bucket name), `region` (AWS region), `access_key`, `secret_key`
|
||||
|
||||
## Examples
|
||||
|
||||
See the [RFS example script](../../rhaiexamples/rfs_example.rhai) for more examples of how to use the RFS module in Rhai scripts.
|
@ -393,7 +393,10 @@ impl PackHero {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Import the std::process::Command directly for some test-specific commands
|
||||
use std::process::Command as StdCommand;
|
||||
use super::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
fn test_platform_detection() {
|
||||
@ -415,7 +418,486 @@ mod tests {
|
||||
assert_eq!(thread_local_debug(), false);
|
||||
}
|
||||
|
||||
// More tests would be added for each platform-specific implementation
|
||||
// These would likely be integration tests that are conditionally compiled
|
||||
// based on the platform they're running on
|
||||
#[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: 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,
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
|
||||
use rhai::{EvalAltResult, Position};
|
||||
use crate::os::{FsError, DownloadError};
|
||||
use crate::os::package::PackageError;
|
||||
|
||||
/// Convert a FsError to a Rhai EvalAltResult
|
||||
pub fn fs_error_to_rhai_error(err: FsError) -> Box<EvalAltResult> {
|
||||
@ -23,6 +24,15 @@ pub fn download_error_to_rhai_error(err: DownloadError) -> Box<EvalAltResult> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert a PackageError to a Rhai EvalAltResult
|
||||
pub fn package_error_to_rhai_error(err: PackageError) -> Box<EvalAltResult> {
|
||||
let err_msg = err.to_string();
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
err_msg.into(),
|
||||
Position::NONE
|
||||
))
|
||||
}
|
||||
|
||||
/// Register error types with the Rhai engine
|
||||
pub fn register_error_types(engine: &mut rhai::Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register helper functions for error handling
|
||||
@ -38,6 +48,10 @@ pub fn register_error_types(engine: &mut rhai::Engine) -> Result<(), Box<EvalAlt
|
||||
format!("Download error: {}", err_msg)
|
||||
});
|
||||
|
||||
engine.register_fn("package_error_message", |err_msg: &str| -> String {
|
||||
format!("Package management error: {}", err_msg)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -57,4 +71,10 @@ impl<T> ToRhaiError<T> for Result<T, DownloadError> {
|
||||
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
|
||||
self.map_err(download_error_to_rhai_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToRhaiError<T> for Result<T, PackageError> {
|
||||
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
|
||||
self.map_err(package_error_to_rhai_error)
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ mod buildah;
|
||||
mod nerdctl;
|
||||
mod git;
|
||||
mod text;
|
||||
mod rfs;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@ -53,6 +54,9 @@ pub use nerdctl::{
|
||||
nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build
|
||||
};
|
||||
|
||||
// Re-export RFS module
|
||||
pub use rfs::register as register_rfs_module;
|
||||
|
||||
// Re-export git module
|
||||
pub use git::register_git_module;
|
||||
pub use crate::git::{GitTree, GitRepo};
|
||||
@ -103,12 +107,16 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
|
||||
// Register Nerdctl module functions
|
||||
nerdctl::register_nerdctl_module(engine)?;
|
||||
|
||||
// Register Git module functions
|
||||
git::register_git_module(engine)?;
|
||||
|
||||
// Register Text module functions
|
||||
text::register_text_module(engine)?;
|
||||
|
||||
// Register RFS module functions
|
||||
rfs::register(engine)?;
|
||||
|
||||
// Future modules can be registered here
|
||||
|
||||
|
||||
|
292
src/rhai/rfs.rs
Normal file
292
src/rhai/rfs.rs
Normal file
@ -0,0 +1,292 @@
|
||||
use rhai::{Engine, EvalAltResult, Map, Array};
|
||||
use crate::virt::rfs::{
|
||||
RfsBuilder, MountType, StoreSpec,
|
||||
list_mounts, unmount_all, unmount, get_mount_info,
|
||||
pack_directory, unpack, list_contents, verify
|
||||
};
|
||||
|
||||
/// Register RFS functions with the Rhai engine
|
||||
pub fn register(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register mount functions
|
||||
engine.register_fn("rfs_mount", rfs_mount);
|
||||
engine.register_fn("rfs_unmount", rfs_unmount);
|
||||
engine.register_fn("rfs_list_mounts", rfs_list_mounts);
|
||||
engine.register_fn("rfs_unmount_all", rfs_unmount_all);
|
||||
engine.register_fn("rfs_get_mount_info", rfs_get_mount_info);
|
||||
|
||||
// Register pack functions
|
||||
engine.register_fn("rfs_pack", rfs_pack);
|
||||
engine.register_fn("rfs_unpack", rfs_unpack);
|
||||
engine.register_fn("rfs_list_contents", rfs_list_contents);
|
||||
engine.register_fn("rfs_verify", rfs_verify);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mount a filesystem
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `source` - Source path or URL
|
||||
/// * `target` - Target mount point
|
||||
/// * `mount_type` - Mount type (e.g., "local", "ssh", "s3", "webdav")
|
||||
/// * `options` - Mount options as a map
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Map, Box<EvalAltResult>>` - Mount information or error
|
||||
fn rfs_mount(source: &str, target: &str, mount_type: &str, options: Map) -> Result<Map, Box<EvalAltResult>> {
|
||||
// Convert mount type string to MountType enum
|
||||
let mount_type_enum = MountType::from_string(mount_type);
|
||||
|
||||
// Create a builder
|
||||
let mut builder = RfsBuilder::new(source, target, mount_type_enum);
|
||||
|
||||
// Add options
|
||||
for (key, value) in options.iter() {
|
||||
if let Ok(value_str) = value.clone().into_string() {
|
||||
builder = builder.with_option(key, &value_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the filesystem
|
||||
let mount = builder.mount()
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to mount filesystem: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))?;
|
||||
|
||||
// Convert Mount to Map
|
||||
let mut result = Map::new();
|
||||
result.insert("id".into(), mount.id.into());
|
||||
result.insert("source".into(), mount.source.into());
|
||||
result.insert("target".into(), mount.target.into());
|
||||
result.insert("fs_type".into(), mount.fs_type.into());
|
||||
|
||||
let options_array: Array = mount.options.iter()
|
||||
.map(|opt| opt.clone().into())
|
||||
.collect();
|
||||
result.insert("options".into(), options_array.into());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Unmount a filesystem
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target` - Target mount point
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_unmount(target: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
unmount(target)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to unmount filesystem: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// List all mounted filesystems
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Array, Box<EvalAltResult>>` - List of mounts or error
|
||||
fn rfs_list_mounts() -> Result<Array, Box<EvalAltResult>> {
|
||||
let mounts = list_mounts()
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to list mounts: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))?;
|
||||
|
||||
let mut result = Array::new();
|
||||
|
||||
for mount in mounts {
|
||||
let mut mount_map = Map::new();
|
||||
mount_map.insert("id".into(), mount.id.into());
|
||||
mount_map.insert("source".into(), mount.source.into());
|
||||
mount_map.insert("target".into(), mount.target.into());
|
||||
mount_map.insert("fs_type".into(), mount.fs_type.into());
|
||||
|
||||
let options_array: Array = mount.options.iter()
|
||||
.map(|opt| opt.clone().into())
|
||||
.collect();
|
||||
mount_map.insert("options".into(), options_array.into());
|
||||
|
||||
result.push(mount_map.into());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Unmount all filesystems
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_unmount_all() -> Result<(), Box<EvalAltResult>> {
|
||||
unmount_all()
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to unmount all filesystems: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Get information about a mounted filesystem
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target` - Target mount point
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Map, Box<EvalAltResult>>` - Mount information or error
|
||||
fn rfs_get_mount_info(target: &str) -> Result<Map, Box<EvalAltResult>> {
|
||||
let mount = get_mount_info(target)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to get mount info: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))?;
|
||||
|
||||
let mut result = Map::new();
|
||||
result.insert("id".into(), mount.id.into());
|
||||
result.insert("source".into(), mount.source.into());
|
||||
result.insert("target".into(), mount.target.into());
|
||||
result.insert("fs_type".into(), mount.fs_type.into());
|
||||
|
||||
let options_array: Array = mount.options.iter()
|
||||
.map(|opt| opt.clone().into())
|
||||
.collect();
|
||||
result.insert("options".into(), options_array.into());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Pack a directory into a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - Directory to pack
|
||||
/// * `output` - Output file
|
||||
/// * `store_specs` - Store specifications as a string (e.g., "file:path=/path/to/store,s3:bucket=my-bucket")
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_pack(directory: &str, output: &str, store_specs: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
// Parse store specs
|
||||
let specs = parse_store_specs(store_specs);
|
||||
|
||||
// Pack the directory
|
||||
pack_directory(directory, output, &specs)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to pack directory: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Unpack a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
/// * `directory` - Directory to unpack to
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_unpack(input: &str, directory: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
unpack(input, directory)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to unpack filesystem layer: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// List the contents of a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<String, Box<EvalAltResult>>` - File listing or error
|
||||
fn rfs_list_contents(input: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
list_contents(input)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to list contents: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Verify a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<bool, Box<EvalAltResult>>` - Whether the layer is valid or error
|
||||
fn rfs_verify(input: &str) -> Result<bool, Box<EvalAltResult>> {
|
||||
verify(input)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to verify filesystem layer: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse store specifications from a string
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `specs_str` - Store specifications as a string
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Vec<StoreSpec>` - Store specifications
|
||||
fn parse_store_specs(specs_str: &str) -> Vec<StoreSpec> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Split by comma
|
||||
for spec_str in specs_str.split(',') {
|
||||
// Skip empty specs
|
||||
if spec_str.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split by colon to get type and options
|
||||
let parts: Vec<&str> = spec_str.split(':').collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get spec type
|
||||
let spec_type = parts[0].trim();
|
||||
|
||||
// Create store spec
|
||||
let mut store_spec = StoreSpec::new(spec_type);
|
||||
|
||||
// Add options if any
|
||||
if parts.len() > 1 {
|
||||
let options_str = parts[1];
|
||||
|
||||
// Split options by comma
|
||||
for option in options_str.split(',') {
|
||||
// Split option by equals sign
|
||||
let option_parts: Vec<&str> = option.split('=').collect();
|
||||
|
||||
if option_parts.len() == 2 {
|
||||
store_spec = store_spec.with_option(option_parts[0].trim(), option_parts[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(store_spec);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
@ -1,27 +1,27 @@
|
||||
// Example script demonstrating the package management functions
|
||||
// Example script demonstrating the mypackage management functions
|
||||
|
||||
// Set debug mode to true to see detailed output
|
||||
package_set_debug(true);
|
||||
|
||||
// Function to demonstrate package management on Ubuntu
|
||||
// Function to demonstrate mypackage management on Ubuntu
|
||||
fn demo_ubuntu() {
|
||||
print("Demonstrating package management on Ubuntu...");
|
||||
print("Demonstrating mypackage management on Ubuntu...");
|
||||
|
||||
// Update package lists
|
||||
print("Updating package lists...");
|
||||
// Update mypackage lists
|
||||
print("Updating mypackage lists...");
|
||||
let result = package_update();
|
||||
print(`Update result: ${result}`);
|
||||
|
||||
// Check if a package is installed
|
||||
let package = "htop";
|
||||
print(`Checking if ${package} is installed...`);
|
||||
let is_installed = package_is_installed(package);
|
||||
print(`${package} is installed: ${is_installed}`);
|
||||
// Check if a mypackage is installed
|
||||
let mypackage = "htop";
|
||||
print(`Checking if ${mypackage} is installed...`);
|
||||
let is_installed = package_is_installed(mypackage);
|
||||
print(`${mypackage} is installed: ${is_installed}`);
|
||||
|
||||
// Install a package if not already installed
|
||||
// Install a mypackage if not already installed
|
||||
if !is_installed {
|
||||
print(`Installing ${package}...`);
|
||||
let install_result = package_install(package);
|
||||
print(`Installing ${mypackage}...`);
|
||||
let install_result = package_install(mypackage);
|
||||
print(`Install result: ${install_result}`);
|
||||
}
|
||||
|
||||
@ -41,33 +41,33 @@ fn demo_ubuntu() {
|
||||
print(` - ${search_results[i]}`);
|
||||
}
|
||||
|
||||
// Remove the package if we installed it
|
||||
// Remove the mypackage if we installed it
|
||||
if !is_installed {
|
||||
print(`Removing ${package}...`);
|
||||
let remove_result = package_remove(package);
|
||||
print(`Removing ${mypackage}...`);
|
||||
let remove_result = package_remove(mypackage);
|
||||
print(`Remove result: ${remove_result}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to demonstrate package management on macOS
|
||||
// Function to demonstrate mypackage management on macOS
|
||||
fn demo_macos() {
|
||||
print("Demonstrating package management on macOS...");
|
||||
print("Demonstrating mypackage management on macOS...");
|
||||
|
||||
// Update package lists
|
||||
print("Updating package lists...");
|
||||
// Update mypackage lists
|
||||
print("Updating mypackage lists...");
|
||||
let result = package_update();
|
||||
print(`Update result: ${result}`);
|
||||
|
||||
// Check if a package is installed
|
||||
let package = "wget";
|
||||
print(`Checking if ${package} is installed...`);
|
||||
let is_installed = package_is_installed(package);
|
||||
print(`${package} is installed: ${is_installed}`);
|
||||
// Check if a mypackage is installed
|
||||
let mypackage = "wget";
|
||||
print(`Checking if ${mypackage} is installed...`);
|
||||
let is_installed = package_is_installed(mypackage);
|
||||
print(`${mypackage} is installed: ${is_installed}`);
|
||||
|
||||
// Install a package if not already installed
|
||||
// Install a mypackage if not already installed
|
||||
if !is_installed {
|
||||
print(`Installing ${package}...`);
|
||||
let install_result = package_install(package);
|
||||
print(`Installing ${mypackage}...`);
|
||||
let install_result = package_install(mypackage);
|
||||
print(`Install result: ${install_result}`);
|
||||
}
|
||||
|
||||
@ -87,10 +87,10 @@ fn demo_macos() {
|
||||
print(` - ${search_results[i]}`);
|
||||
}
|
||||
|
||||
// Remove the package if we installed it
|
||||
// Remove the mypackage if we installed it
|
||||
if !is_installed {
|
||||
print(`Removing ${package}...`);
|
||||
let remove_result = package_remove(package);
|
||||
print(`Removing ${mypackage}...`);
|
||||
let remove_result = package_remove(mypackage);
|
||||
print(`Remove result: ${remove_result}`);
|
||||
}
|
||||
}
|
||||
|
121
src/rhaiexamples/rfs_example.rhai
Normal file
121
src/rhaiexamples/rfs_example.rhai
Normal file
@ -0,0 +1,121 @@
|
||||
// RFS Example Script
|
||||
// This script demonstrates how to use the RFS wrapper in Rhai
|
||||
|
||||
// Mount a local directory
|
||||
fn mount_local_example() {
|
||||
print("Mounting a local directory...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("/source/path", "/target/path", "local", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// List all mounts
|
||||
let mounts = rfs_list_mounts();
|
||||
print(`Number of mounts: ${mounts.len()}`);
|
||||
|
||||
for mount in mounts {
|
||||
print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`);
|
||||
}
|
||||
|
||||
// Unmount the directory
|
||||
rfs_unmount("/target/path");
|
||||
print("Unmounted the directory");
|
||||
}
|
||||
|
||||
// Pack a directory into a filesystem layer
|
||||
fn pack_example() {
|
||||
print("Packing a directory into a filesystem layer...");
|
||||
|
||||
// Pack the directory
|
||||
// Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket"
|
||||
rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store");
|
||||
|
||||
print("Directory packed successfully");
|
||||
|
||||
// List the contents of the filesystem layer
|
||||
let contents = rfs_list_contents("output.fl");
|
||||
print("Contents of the filesystem layer:");
|
||||
print(contents);
|
||||
|
||||
// Verify the filesystem layer
|
||||
let is_valid = rfs_verify("output.fl");
|
||||
print(`Is the filesystem layer valid? ${is_valid}`);
|
||||
|
||||
// Unpack the filesystem layer
|
||||
rfs_unpack("output.fl", "/path/to/unpack");
|
||||
print("Filesystem layer unpacked successfully");
|
||||
}
|
||||
|
||||
// SSH mount example
|
||||
fn mount_ssh_example() {
|
||||
print("Mounting a remote directory via SSH...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"port": "22",
|
||||
"identity_file": "/path/to/key",
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// Get mount info
|
||||
let info = rfs_get_mount_info("/local/mount/point");
|
||||
print(`Mount info: ${info}`);
|
||||
|
||||
// Unmount the directory
|
||||
rfs_unmount("/local/mount/point");
|
||||
print("Unmounted the directory");
|
||||
}
|
||||
|
||||
// S3 mount example
|
||||
fn mount_s3_example() {
|
||||
print("Mounting an S3 bucket...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"region": "us-east-1",
|
||||
"access_key": "your-access-key",
|
||||
"secret_key": "your-secret-key"
|
||||
};
|
||||
|
||||
// Mount the S3 bucket
|
||||
let mount = rfs_mount("s3://my-bucket", "/mnt/s3", "s3", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// Unmount the S3 bucket
|
||||
rfs_unmount("/mnt/s3");
|
||||
print("Unmounted the S3 bucket");
|
||||
}
|
||||
|
||||
// Unmount all example
|
||||
fn unmount_all_example() {
|
||||
print("Unmounting all filesystems...");
|
||||
|
||||
// Unmount all filesystems
|
||||
rfs_unmount_all();
|
||||
|
||||
print("All filesystems unmounted");
|
||||
}
|
||||
|
||||
// Run the examples
|
||||
// Note: These are commented out to prevent accidental execution
|
||||
// Uncomment the ones you want to run
|
||||
|
||||
// mount_local_example();
|
||||
// pack_example();
|
||||
// mount_ssh_example();
|
||||
// mount_s3_example();
|
||||
// unmount_all_example();
|
||||
|
||||
print("RFS example script completed");
|
@ -1,2 +1,3 @@
|
||||
pub mod buildah;
|
||||
pub mod nerdctl;
|
||||
pub mod nerdctl;
|
||||
pub mod rfs;
|
280
src/virt/rfs/builder.rs
Normal file
280
src/virt/rfs/builder.rs
Normal file
@ -0,0 +1,280 @@
|
||||
use std::collections::HashMap;
|
||||
use super::{
|
||||
error::RfsError,
|
||||
cmd::execute_rfs_command,
|
||||
types::{Mount, MountType, StoreSpec},
|
||||
};
|
||||
|
||||
/// Builder for RFS mount operations
|
||||
#[derive(Clone)]
|
||||
pub struct RfsBuilder {
|
||||
/// Source path or URL
|
||||
source: String,
|
||||
/// Target mount point
|
||||
target: String,
|
||||
/// Mount type
|
||||
mount_type: MountType,
|
||||
/// Mount options
|
||||
options: HashMap<String, String>,
|
||||
/// Mount ID
|
||||
mount_id: Option<String>,
|
||||
/// Debug mode
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl RfsBuilder {
|
||||
/// Create a new RFS builder
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `source` - Source path or URL
|
||||
/// * `target` - Target mount point
|
||||
/// * `mount_type` - Mount type
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - New RFS builder
|
||||
pub fn new(source: &str, target: &str, mount_type: MountType) -> Self {
|
||||
Self {
|
||||
source: source.to_string(),
|
||||
target: target.to_string(),
|
||||
mount_type,
|
||||
options: HashMap::new(),
|
||||
mount_id: None,
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a mount option
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - Option key
|
||||
/// * `value` - Option value
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - Updated RFS builder for method chaining
|
||||
pub fn with_option(mut self, key: &str, value: &str) -> Self {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple mount options
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `options` - Map of option keys to values
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - Updated RFS builder for method chaining
|
||||
pub fn with_options(mut self, options: HashMap<&str, &str>) -> Self {
|
||||
for (key, value) in options {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set debug mode
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - Updated RFS builder for method chaining
|
||||
pub fn with_debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
/// Mount the filesystem
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Mount, RfsError>` - Mount information or error
|
||||
pub fn mount(self) -> Result<Mount, RfsError> {
|
||||
// Build the command string
|
||||
let mut cmd = String::from("mount -t ");
|
||||
cmd.push_str(&self.mount_type.to_string());
|
||||
|
||||
// Add options if any
|
||||
if !self.options.is_empty() {
|
||||
cmd.push_str(" -o ");
|
||||
let mut first = true;
|
||||
for (key, value) in &self.options {
|
||||
if !first {
|
||||
cmd.push_str(",");
|
||||
}
|
||||
cmd.push_str(key);
|
||||
cmd.push_str("=");
|
||||
cmd.push_str(value);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add source and target
|
||||
cmd.push_str(" ");
|
||||
cmd.push_str(&self.source);
|
||||
cmd.push_str(" ");
|
||||
cmd.push_str(&self.target);
|
||||
|
||||
// Split the command into arguments
|
||||
let args: Vec<&str> = cmd.split_whitespace().collect();
|
||||
|
||||
// Execute the command
|
||||
let result = execute_rfs_command(&args)?;
|
||||
|
||||
// Parse the output to get the mount ID
|
||||
let mount_id = result.stdout.trim().to_string();
|
||||
if mount_id.is_empty() {
|
||||
return Err(RfsError::MountFailed("Failed to get mount ID".to_string()));
|
||||
}
|
||||
|
||||
// Create and return the Mount struct
|
||||
Ok(Mount {
|
||||
id: mount_id,
|
||||
source: self.source,
|
||||
target: self.target,
|
||||
fs_type: self.mount_type.to_string(),
|
||||
options: self.options.iter().map(|(k, v)| format!("{}={}", k, v)).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Unmount the filesystem
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), RfsError>` - Success or error
|
||||
pub fn unmount(&self) -> Result<(), RfsError> {
|
||||
// Execute the unmount command
|
||||
let result = execute_rfs_command(&["unmount", &self.target])?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::UnmountFailed(format!("Failed to unmount {}: {}", self.target, result.stderr)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for RFS pack operations
|
||||
#[derive(Clone)]
|
||||
pub struct PackBuilder {
|
||||
/// Directory to pack
|
||||
directory: String,
|
||||
/// Output file
|
||||
output: String,
|
||||
/// Store specifications
|
||||
store_specs: Vec<StoreSpec>,
|
||||
/// Debug mode
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl PackBuilder {
|
||||
/// Create a new pack builder
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - Directory to pack
|
||||
/// * `output` - Output file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - New pack builder
|
||||
pub fn new(directory: &str, output: &str) -> Self {
|
||||
Self {
|
||||
directory: directory.to_string(),
|
||||
output: output.to_string(),
|
||||
store_specs: Vec::new(),
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a store specification
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store_spec` - Store specification
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - Updated pack builder for method chaining
|
||||
pub fn with_store_spec(mut self, store_spec: StoreSpec) -> Self {
|
||||
self.store_specs.push(store_spec);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple store specifications
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store_specs` - Store specifications
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - Updated pack builder for method chaining
|
||||
pub fn with_store_specs(mut self, store_specs: Vec<StoreSpec>) -> Self {
|
||||
self.store_specs.extend(store_specs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set debug mode
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - Updated pack builder for method chaining
|
||||
pub fn with_debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
/// Pack the directory
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), RfsError>` - Success or error
|
||||
pub fn pack(self) -> Result<(), RfsError> {
|
||||
// Build the command string
|
||||
let mut cmd = String::from("pack -m ");
|
||||
cmd.push_str(&self.output);
|
||||
|
||||
// Add store specs if any
|
||||
if !self.store_specs.is_empty() {
|
||||
cmd.push_str(" -s ");
|
||||
let mut first = true;
|
||||
for spec in &self.store_specs {
|
||||
if !first {
|
||||
cmd.push_str(",");
|
||||
}
|
||||
let spec_str = spec.to_string();
|
||||
cmd.push_str(&spec_str);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add directory
|
||||
cmd.push_str(" ");
|
||||
cmd.push_str(&self.directory);
|
||||
|
||||
// Split the command into arguments
|
||||
let args: Vec<&str> = cmd.split_whitespace().collect();
|
||||
|
||||
// Execute the command
|
||||
let result = execute_rfs_command(&args)?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::PackFailed(format!("Failed to pack {}: {}", self.directory, result.stderr)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
62
src/virt/rfs/cmd.rs
Normal file
62
src/virt/rfs/cmd.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use crate::process::{run_command, CommandResult};
|
||||
use super::error::RfsError;
|
||||
use std::thread_local;
|
||||
use std::cell::RefCell;
|
||||
|
||||
// Thread-local storage for debug flag
|
||||
thread_local! {
|
||||
static DEBUG: RefCell<bool> = RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the thread-local debug flag
|
||||
pub fn set_thread_local_debug(debug: bool) {
|
||||
DEBUG.with(|d| {
|
||||
*d.borrow_mut() = debug;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the current thread-local debug flag
|
||||
pub fn thread_local_debug() -> bool {
|
||||
DEBUG.with(|d| {
|
||||
*d.borrow()
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute an RFS command with the given arguments
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - Command arguments
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, RfsError>` - Command result or error
|
||||
pub fn execute_rfs_command(args: &[&str]) -> Result<CommandResult, RfsError> {
|
||||
let debug = thread_local_debug();
|
||||
|
||||
// Construct the command string
|
||||
let mut cmd = String::from("rfs");
|
||||
for arg in args {
|
||||
cmd.push(' ');
|
||||
cmd.push_str(arg);
|
||||
}
|
||||
|
||||
if debug {
|
||||
println!("Executing RFS command: {}", cmd);
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
let result = run_command(&cmd)
|
||||
.map_err(|e| RfsError::CommandFailed(format!("Failed to execute RFS command: {}", e)))?;
|
||||
|
||||
if debug {
|
||||
println!("RFS command result: {:?}", result);
|
||||
}
|
||||
|
||||
// Check if the command was successful
|
||||
if !result.success && !result.stderr.is_empty() {
|
||||
return Err(RfsError::CommandFailed(result.stderr));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
43
src/virt/rfs/error.rs
Normal file
43
src/virt/rfs/error.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
|
||||
/// Error types for RFS operations
|
||||
#[derive(Debug)]
|
||||
pub enum RfsError {
|
||||
/// Command execution failed
|
||||
CommandFailed(String),
|
||||
/// Invalid argument provided
|
||||
InvalidArgument(String),
|
||||
/// Mount operation failed
|
||||
MountFailed(String),
|
||||
/// Unmount operation failed
|
||||
UnmountFailed(String),
|
||||
/// List operation failed
|
||||
ListFailed(String),
|
||||
/// Pack operation failed
|
||||
PackFailed(String),
|
||||
/// Other error
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for RfsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RfsError::CommandFailed(msg) => write!(f, "RFS command failed: {}", msg),
|
||||
RfsError::InvalidArgument(msg) => write!(f, "Invalid argument: {}", msg),
|
||||
RfsError::MountFailed(msg) => write!(f, "Mount failed: {}", msg),
|
||||
RfsError::UnmountFailed(msg) => write!(f, "Unmount failed: {}", msg),
|
||||
RfsError::ListFailed(msg) => write!(f, "List failed: {}", msg),
|
||||
RfsError::PackFailed(msg) => write!(f, "Pack failed: {}", msg),
|
||||
RfsError::Other(msg) => write!(f, "Other error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for RfsError {}
|
||||
|
||||
impl From<std::io::Error> for RfsError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
RfsError::Other(format!("IO error: {}", error))
|
||||
}
|
||||
}
|
15
src/virt/rfs/mod.rs
Normal file
15
src/virt/rfs/mod.rs
Normal file
@ -0,0 +1,15 @@
|
||||
mod cmd;
|
||||
mod error;
|
||||
mod mount;
|
||||
mod pack;
|
||||
mod builder;
|
||||
mod types;
|
||||
|
||||
pub use error::RfsError;
|
||||
pub use builder::{RfsBuilder, PackBuilder};
|
||||
pub use types::{Mount, MountType, StoreSpec};
|
||||
pub use mount::{list_mounts, unmount_all, unmount, get_mount_info};
|
||||
pub use pack::{pack_directory, unpack, list_contents, verify};
|
||||
|
||||
// Re-export the execute_rfs_command function for use in other modules
|
||||
pub(crate) use cmd::execute_rfs_command;
|
142
src/virt/rfs/mount.rs
Normal file
142
src/virt/rfs/mount.rs
Normal file
@ -0,0 +1,142 @@
|
||||
use super::{
|
||||
error::RfsError,
|
||||
cmd::execute_rfs_command,
|
||||
types::Mount,
|
||||
};
|
||||
|
||||
/// List all mounted filesystems
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Vec<Mount>, RfsError>` - List of mounts or error
|
||||
pub fn list_mounts() -> Result<Vec<Mount>, RfsError> {
|
||||
// Execute the list command
|
||||
let result = execute_rfs_command(&["list", "--json"])?;
|
||||
|
||||
// Parse the JSON output
|
||||
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
|
||||
Ok(json) => {
|
||||
if let serde_json::Value::Array(mounts_json) = json {
|
||||
let mut mounts = Vec::new();
|
||||
|
||||
for mount_json in mounts_json {
|
||||
// Extract mount ID
|
||||
let id = match mount_json.get("id").and_then(|v| v.as_str()) {
|
||||
Some(id) => id.to_string(),
|
||||
None => return Err(RfsError::ListFailed("Missing mount ID".to_string())),
|
||||
};
|
||||
|
||||
// Extract source
|
||||
let source = match mount_json.get("source").and_then(|v| v.as_str()) {
|
||||
Some(source) => source.to_string(),
|
||||
None => return Err(RfsError::ListFailed("Missing source".to_string())),
|
||||
};
|
||||
|
||||
// Extract target
|
||||
let target = match mount_json.get("target").and_then(|v| v.as_str()) {
|
||||
Some(target) => target.to_string(),
|
||||
None => return Err(RfsError::ListFailed("Missing target".to_string())),
|
||||
};
|
||||
|
||||
// Extract filesystem type
|
||||
let fs_type = match mount_json.get("type").and_then(|v| v.as_str()) {
|
||||
Some(fs_type) => fs_type.to_string(),
|
||||
None => return Err(RfsError::ListFailed("Missing filesystem type".to_string())),
|
||||
};
|
||||
|
||||
// Extract options
|
||||
let options = match mount_json.get("options").and_then(|v| v.as_array()) {
|
||||
Some(options_array) => {
|
||||
let mut options_vec = Vec::new();
|
||||
for option_value in options_array {
|
||||
if let Some(option_str) = option_value.as_str() {
|
||||
options_vec.push(option_str.to_string());
|
||||
}
|
||||
}
|
||||
options_vec
|
||||
},
|
||||
None => Vec::new(), // Empty vector if no options found
|
||||
};
|
||||
|
||||
// Create Mount struct and add to vector
|
||||
mounts.push(Mount {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
fs_type,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(mounts)
|
||||
} else {
|
||||
Err(RfsError::ListFailed("Expected JSON array".to_string()))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
Err(RfsError::ListFailed(format!("Failed to parse mount list JSON: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unmount a filesystem by target path
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target` - Target mount point
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), RfsError>` - Success or error
|
||||
pub fn unmount(target: &str) -> Result<(), RfsError> {
|
||||
// Execute the unmount command
|
||||
let result = execute_rfs_command(&["unmount", target])?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::UnmountFailed(format!("Failed to unmount {}: {}", target, result.stderr)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmount all filesystems
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), RfsError>` - Success or error
|
||||
pub fn unmount_all() -> Result<(), RfsError> {
|
||||
// Execute the unmount all command
|
||||
let result = execute_rfs_command(&["unmount", "--all"])?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::UnmountFailed(format!("Failed to unmount all filesystems: {}", result.stderr)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get information about a mounted filesystem
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target` - Target mount point
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Mount, RfsError>` - Mount information or error
|
||||
pub fn get_mount_info(target: &str) -> Result<Mount, RfsError> {
|
||||
// Get all mounts
|
||||
let mounts = list_mounts()?;
|
||||
|
||||
// Find the mount with the specified target
|
||||
for mount in mounts {
|
||||
if mount.target == target {
|
||||
return Ok(mount);
|
||||
}
|
||||
}
|
||||
|
||||
// Mount not found
|
||||
Err(RfsError::Other(format!("No mount found at {}", target)))
|
||||
}
|
100
src/virt/rfs/pack.rs
Normal file
100
src/virt/rfs/pack.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use super::{
|
||||
error::RfsError,
|
||||
cmd::execute_rfs_command,
|
||||
types::StoreSpec,
|
||||
builder::PackBuilder,
|
||||
};
|
||||
|
||||
/// Pack a directory into a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - Directory to pack
|
||||
/// * `output` - Output file
|
||||
/// * `store_specs` - Store specifications
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), RfsError>` - Success or error
|
||||
pub fn pack_directory(directory: &str, output: &str, store_specs: &[StoreSpec]) -> Result<(), RfsError> {
|
||||
// Create a new pack builder
|
||||
let mut builder = PackBuilder::new(directory, output);
|
||||
|
||||
// Add store specs
|
||||
for spec in store_specs {
|
||||
builder = builder.with_store_spec(spec.clone());
|
||||
}
|
||||
|
||||
// Pack the directory
|
||||
builder.pack()
|
||||
}
|
||||
|
||||
/// Unpack a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
/// * `directory` - Directory to unpack to
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), RfsError>` - Success or error
|
||||
pub fn unpack(input: &str, directory: &str) -> Result<(), RfsError> {
|
||||
// Execute the unpack command
|
||||
let result = execute_rfs_command(&["unpack", "-m", input, directory])?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::Other(format!("Failed to unpack {}: {}", input, result.stderr)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List the contents of a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<String, RfsError>` - File listing or error
|
||||
pub fn list_contents(input: &str) -> Result<String, RfsError> {
|
||||
// Execute the list command
|
||||
let result = execute_rfs_command(&["list", "-m", input])?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::Other(format!("Failed to list contents of {}: {}", input, result.stderr)));
|
||||
}
|
||||
|
||||
Ok(result.stdout)
|
||||
}
|
||||
|
||||
/// Verify a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<bool, RfsError>` - Whether the layer is valid or error
|
||||
pub fn verify(input: &str) -> Result<bool, RfsError> {
|
||||
// Execute the verify command
|
||||
let result = execute_rfs_command(&["verify", "-m", input])?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
// If the command failed but returned a specific error about verification,
|
||||
// return false instead of an error
|
||||
if result.stderr.contains("verification failed") {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
return Err(RfsError::Other(format!("Failed to verify {}: {}", input, result.stderr)));
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
117
src/virt/rfs/types.rs
Normal file
117
src/virt/rfs/types.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents a mounted filesystem
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mount {
|
||||
/// Mount ID
|
||||
pub id: String,
|
||||
/// Source path or URL
|
||||
pub source: String,
|
||||
/// Target mount point
|
||||
pub target: String,
|
||||
/// Filesystem type
|
||||
pub fs_type: String,
|
||||
/// Mount options
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
/// Types of mounts supported by RFS
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MountType {
|
||||
/// Local filesystem
|
||||
Local,
|
||||
/// SSH remote filesystem
|
||||
SSH,
|
||||
/// S3 object storage
|
||||
S3,
|
||||
/// WebDAV remote filesystem
|
||||
WebDAV,
|
||||
/// Custom mount type
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl MountType {
|
||||
/// Convert mount type to string representation
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
MountType::Local => "local".to_string(),
|
||||
MountType::SSH => "ssh".to_string(),
|
||||
MountType::S3 => "s3".to_string(),
|
||||
MountType::WebDAV => "webdav".to_string(),
|
||||
MountType::Custom(s) => s.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a MountType from a string
|
||||
pub fn from_string(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"local" => MountType::Local,
|
||||
"ssh" => MountType::SSH,
|
||||
"s3" => MountType::S3,
|
||||
"webdav" => MountType::WebDAV,
|
||||
_ => MountType::Custom(s.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store specification for packing operations
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StoreSpec {
|
||||
/// Store type (e.g., "file", "s3")
|
||||
pub spec_type: String,
|
||||
/// Store options
|
||||
pub options: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl StoreSpec {
|
||||
/// Create a new store specification
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `spec_type` - Store type (e.g., "file", "s3")
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - New store specification
|
||||
pub fn new(spec_type: &str) -> Self {
|
||||
Self {
|
||||
spec_type: spec_type.to_string(),
|
||||
options: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an option to the store specification
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - Option key
|
||||
/// * `value` - Option value
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Self` - Updated store specification for method chaining
|
||||
pub fn with_option(mut self, key: &str, value: &str) -> Self {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Convert the store specification to a string
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `String` - String representation of the store specification
|
||||
pub fn to_string(&self) -> String {
|
||||
let mut result = self.spec_type.clone();
|
||||
|
||||
if !self.options.is_empty() {
|
||||
result.push_str(":");
|
||||
let options: Vec<String> = self.options
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}={}", k, v))
|
||||
.collect();
|
||||
result.push_str(&options.join(","));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user