mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
551 lines
20 KiB
Rust
551 lines
20 KiB
Rust
use keyring::Entry;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::error::Error;
|
|
|
|
/// Configuration for the RustFS service
|
|
///
|
|
/// # Fields
|
|
/// * `address` - The address of the RustFS service
|
|
/// * `host` - The host of the RustFS service
|
|
/// * `port` - The port of the RustFS service
|
|
/// * `access_key` - The access key of the RustFS service
|
|
/// * `secret_key` - The secret key of the RustFS service
|
|
/// * `domain_name` - The domain name of the RustFS service
|
|
/// * `volume_name` - The volume name of the RustFS service
|
|
/// * `console_address` - The console address of the RustFS service
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// let config = RustFSConfig {
|
|
/// address: "127.0.0.1:9000".to_string(),
|
|
/// host: "127.0.0.1".to_string(),
|
|
/// port: "9000".to_string(),
|
|
/// access_key: "rustfsadmin".to_string(),
|
|
/// secret_key: "rustfsadmin".to_string(),
|
|
/// domain_name: "demo.rustfs.com".to_string(),
|
|
/// volume_name: "data".to_string(),
|
|
/// console_address: "127.0.0.1:9001".to_string(),
|
|
/// };
|
|
/// println!("{:?}", config);
|
|
/// assert_eq!(config.address, "127.0.0.1:9000");
|
|
/// ```
|
|
#[derive(Debug, Clone, Default, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)]
|
|
pub struct RustFSConfig {
|
|
pub address: String,
|
|
pub host: String,
|
|
pub port: String,
|
|
pub access_key: String,
|
|
pub secret_key: String,
|
|
pub domain_name: String,
|
|
pub volume_name: String,
|
|
pub console_address: String,
|
|
}
|
|
|
|
impl RustFSConfig {
|
|
/// keyring the name of the service
|
|
const SERVICE_NAME: &'static str = "rustfs-service";
|
|
/// keyring the key of the service
|
|
const SERVICE_KEY: &'static str = "rustfs_key";
|
|
/// default domain name
|
|
const DEFAULT_DOMAIN_NAME_VALUE: &'static str = "demo.rustfs.com";
|
|
/// default address value
|
|
const DEFAULT_ADDRESS_VALUE: &'static str = "127.0.0.1:9000";
|
|
/// default port value
|
|
const DEFAULT_PORT_VALUE: &'static str = "9000";
|
|
/// default host value
|
|
const DEFAULT_HOST_VALUE: &'static str = "127.0.0.1";
|
|
/// default access key value
|
|
const DEFAULT_ACCESS_KEY_VALUE: &'static str = "rustfsadmin";
|
|
/// default secret key value
|
|
const DEFAULT_SECRET_KEY_VALUE: &'static str = "rustfsadmin";
|
|
/// default console address value
|
|
const DEFAULT_CONSOLE_ADDRESS_VALUE: &'static str = "127.0.0.1:9001";
|
|
|
|
/// get the default volume_name
|
|
///
|
|
/// # Returns
|
|
/// * The default volume name
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// let volume_name = RustFSConfig::default_volume_name();
|
|
/// ```
|
|
pub fn default_volume_name() -> String {
|
|
dirs::home_dir()
|
|
.map(|home| home.join("rustfs").join("data"))
|
|
.and_then(|path| path.to_str().map(String::from))
|
|
.unwrap_or_else(|| "data".to_string())
|
|
}
|
|
|
|
/// create a default configuration
|
|
///
|
|
/// # Returns
|
|
/// * The default configuration
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// let config = RustFSConfig::default_config();
|
|
/// println!("{:?}", config);
|
|
/// assert_eq!(config.address, "127.0.0.1:9000");
|
|
/// ```
|
|
pub fn default_config() -> Self {
|
|
Self {
|
|
address: Self::DEFAULT_ADDRESS_VALUE.to_string(),
|
|
host: Self::DEFAULT_HOST_VALUE.to_string(),
|
|
port: Self::DEFAULT_PORT_VALUE.to_string(),
|
|
access_key: Self::DEFAULT_ACCESS_KEY_VALUE.to_string(),
|
|
secret_key: Self::DEFAULT_SECRET_KEY_VALUE.to_string(),
|
|
domain_name: Self::DEFAULT_DOMAIN_NAME_VALUE.to_string(),
|
|
volume_name: Self::default_volume_name(),
|
|
console_address: Self::DEFAULT_CONSOLE_ADDRESS_VALUE.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Load the configuration from the keyring
|
|
///
|
|
/// # Errors
|
|
/// * If the configuration cannot be loaded from the keyring
|
|
/// * If the configuration cannot be deserialized
|
|
/// * If the address cannot be extracted from the configuration
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// let config = RustFSConfig::load().unwrap();
|
|
/// println!("{:?}", config);
|
|
/// assert_eq!(config.address, "127.0.0.1:9000");
|
|
/// ```
|
|
pub fn load() -> Result<Self, Box<dyn Error>> {
|
|
let mut config = Self::default_config();
|
|
|
|
// Try to get the configuration of the storage from the keyring
|
|
let entry = Entry::new(Self::SERVICE_NAME, Self::SERVICE_KEY)?;
|
|
if let Ok(stored_json) = entry.get_password() {
|
|
if let Ok(stored_config) = serde_json::from_str::<RustFSConfig>(&stored_json) {
|
|
// update fields that are not empty and non default
|
|
if !stored_config.address.is_empty() && stored_config.address != Self::DEFAULT_ADDRESS_VALUE {
|
|
config.address = stored_config.address;
|
|
let (host, port) = Self::extract_host_port(config.address.as_str())
|
|
.ok_or_else(|| format!("无法从地址 '{}' 中提取主机和端口", config.address))?;
|
|
config.host = host.to_string();
|
|
config.port = port.to_string();
|
|
}
|
|
if !stored_config.access_key.is_empty() && stored_config.access_key != Self::DEFAULT_ACCESS_KEY_VALUE {
|
|
config.access_key = stored_config.access_key;
|
|
}
|
|
if !stored_config.secret_key.is_empty() && stored_config.secret_key != Self::DEFAULT_SECRET_KEY_VALUE {
|
|
config.secret_key = stored_config.secret_key;
|
|
}
|
|
if !stored_config.domain_name.is_empty() && stored_config.domain_name != Self::DEFAULT_DOMAIN_NAME_VALUE {
|
|
config.domain_name = stored_config.domain_name;
|
|
}
|
|
// The stored volume_name is updated only if it is not empty and different from the default
|
|
if !stored_config.volume_name.is_empty() && stored_config.volume_name != Self::default_volume_name() {
|
|
config.volume_name = stored_config.volume_name;
|
|
}
|
|
if !stored_config.console_address.is_empty()
|
|
&& stored_config.console_address != Self::DEFAULT_CONSOLE_ADDRESS_VALUE
|
|
{
|
|
config.console_address = stored_config.console_address;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
/// Auxiliary method: Extract the host and port from the address string
|
|
/// # Arguments
|
|
/// * `address` - The address string
|
|
///
|
|
/// # Returns
|
|
/// * `Some((host, port))` - The host and port
|
|
///
|
|
/// # Errors
|
|
/// * If the address is not in the form 'host:port'
|
|
/// * If the port is not a valid u16
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// let (host, port) = RustFSConfig::extract_host_port("127.0.0.1:9000").unwrap();
|
|
/// assert_eq!(host, "127.0.0.1");
|
|
/// assert_eq!(port, 9000);
|
|
/// ```
|
|
pub fn extract_host_port(address: &str) -> Option<(&str, u16)> {
|
|
let parts: Vec<&str> = address.split(':').collect();
|
|
if parts.len() == 2 {
|
|
if let Ok(port) = parts[1].parse::<u16>() {
|
|
return Some((parts[0], port));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// save the configuration to keyring
|
|
///
|
|
/// # Errors
|
|
/// * If the configuration cannot be serialized
|
|
/// * If the configuration cannot be saved to the keyring
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// let config = RustFSConfig::default_config();
|
|
/// config.save().unwrap();
|
|
/// ```
|
|
pub fn save(&self) -> Result<(), Box<dyn Error>> {
|
|
let entry = Entry::new(Self::SERVICE_NAME, Self::SERVICE_KEY)?;
|
|
let json = serde_json::to_string(self)?;
|
|
entry.set_password(&json)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Clear the stored configuration from the system keyring
|
|
///
|
|
/// # Returns
|
|
/// Returns `Ok(())` if the configuration was successfully cleared, or an error if the operation failed.
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// RustFSConfig::clear().unwrap();
|
|
/// ```
|
|
#[allow(dead_code)]
|
|
pub fn clear() -> Result<(), Box<dyn Error>> {
|
|
let entry = Entry::new(Self::SERVICE_NAME, Self::SERVICE_KEY)?;
|
|
entry.delete_credential()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_rustfs_config_default() {
|
|
let config = RustFSConfig::default();
|
|
assert!(config.address.is_empty());
|
|
assert!(config.host.is_empty());
|
|
assert!(config.port.is_empty());
|
|
assert!(config.access_key.is_empty());
|
|
assert!(config.secret_key.is_empty());
|
|
assert!(config.domain_name.is_empty());
|
|
assert!(config.volume_name.is_empty());
|
|
assert!(config.console_address.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_rustfs_config_creation() {
|
|
let config = RustFSConfig {
|
|
address: "192.168.1.100:9000".to_string(),
|
|
host: "192.168.1.100".to_string(),
|
|
port: "9000".to_string(),
|
|
access_key: "testuser".to_string(),
|
|
secret_key: "testpass".to_string(),
|
|
domain_name: "test.rustfs.com".to_string(),
|
|
volume_name: "/data/rustfs".to_string(),
|
|
console_address: "192.168.1.100:9001".to_string(),
|
|
};
|
|
|
|
assert_eq!(config.address, "192.168.1.100:9000");
|
|
assert_eq!(config.host, "192.168.1.100");
|
|
assert_eq!(config.port, "9000");
|
|
assert_eq!(config.access_key, "testuser");
|
|
assert_eq!(config.secret_key, "testpass");
|
|
assert_eq!(config.domain_name, "test.rustfs.com");
|
|
assert_eq!(config.volume_name, "/data/rustfs");
|
|
assert_eq!(config.console_address, "192.168.1.100:9001");
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_volume_name() {
|
|
let volume_name = RustFSConfig::default_volume_name();
|
|
assert!(!volume_name.is_empty());
|
|
// Should either be the home directory path or fallback to "data"
|
|
assert!(volume_name.contains("rustfs") || volume_name == "data");
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_config() {
|
|
let config = RustFSConfig::default_config();
|
|
assert_eq!(config.address, RustFSConfig::DEFAULT_ADDRESS_VALUE);
|
|
assert_eq!(config.host, RustFSConfig::DEFAULT_HOST_VALUE);
|
|
assert_eq!(config.port, RustFSConfig::DEFAULT_PORT_VALUE);
|
|
assert_eq!(config.access_key, RustFSConfig::DEFAULT_ACCESS_KEY_VALUE);
|
|
assert_eq!(config.secret_key, RustFSConfig::DEFAULT_SECRET_KEY_VALUE);
|
|
assert_eq!(config.domain_name, RustFSConfig::DEFAULT_DOMAIN_NAME_VALUE);
|
|
assert_eq!(config.console_address, RustFSConfig::DEFAULT_CONSOLE_ADDRESS_VALUE);
|
|
assert!(!config.volume_name.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_host_port_valid() {
|
|
let test_cases = vec![
|
|
("127.0.0.1:9000", Some(("127.0.0.1", 9000))),
|
|
("localhost:8080", Some(("localhost", 8080))),
|
|
("192.168.1.100:3000", Some(("192.168.1.100", 3000))),
|
|
("0.0.0.0:80", Some(("0.0.0.0", 80))),
|
|
("example.com:443", Some(("example.com", 443))),
|
|
];
|
|
|
|
for (input, expected) in test_cases {
|
|
let result = RustFSConfig::extract_host_port(input);
|
|
assert_eq!(result, expected, "Failed for input: {}", input);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_host_port_invalid() {
|
|
let invalid_cases = vec![
|
|
"127.0.0.1", // Missing port
|
|
"127.0.0.1:", // Empty port
|
|
"127.0.0.1:abc", // Invalid port
|
|
"127.0.0.1:99999", // Port out of range
|
|
"", // Empty string
|
|
"127.0.0.1:9000:extra", // Too many parts
|
|
"invalid", // No colon
|
|
];
|
|
|
|
for input in invalid_cases {
|
|
let result = RustFSConfig::extract_host_port(input);
|
|
assert_eq!(result, None, "Should be None for input: {}", input);
|
|
}
|
|
|
|
// Special case: empty host but valid port should still work
|
|
let result = RustFSConfig::extract_host_port(":9000");
|
|
assert_eq!(result, Some(("", 9000)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_host_port_edge_cases() {
|
|
// Test edge cases for port numbers
|
|
assert_eq!(RustFSConfig::extract_host_port("host:0"), Some(("host", 0)));
|
|
assert_eq!(RustFSConfig::extract_host_port("host:65535"), Some(("host", 65535)));
|
|
assert_eq!(RustFSConfig::extract_host_port("host:65536"), None); // Out of range
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialization() {
|
|
let config = RustFSConfig {
|
|
address: "127.0.0.1:9000".to_string(),
|
|
host: "127.0.0.1".to_string(),
|
|
port: "9000".to_string(),
|
|
access_key: "admin".to_string(),
|
|
secret_key: "password".to_string(),
|
|
domain_name: "test.com".to_string(),
|
|
volume_name: "/data".to_string(),
|
|
console_address: "127.0.0.1:9001".to_string(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&config).unwrap();
|
|
assert!(json.contains("127.0.0.1:9000"));
|
|
assert!(json.contains("admin"));
|
|
assert!(json.contains("test.com"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialization() {
|
|
let json = r#"{
|
|
"address": "192.168.1.100:9000",
|
|
"host": "192.168.1.100",
|
|
"port": "9000",
|
|
"access_key": "testuser",
|
|
"secret_key": "testpass",
|
|
"domain_name": "example.com",
|
|
"volume_name": "/opt/data",
|
|
"console_address": "192.168.1.100:9001"
|
|
}"#;
|
|
|
|
let config: RustFSConfig = serde_json::from_str(json).unwrap();
|
|
assert_eq!(config.address, "192.168.1.100:9000");
|
|
assert_eq!(config.host, "192.168.1.100");
|
|
assert_eq!(config.port, "9000");
|
|
assert_eq!(config.access_key, "testuser");
|
|
assert_eq!(config.secret_key, "testpass");
|
|
assert_eq!(config.domain_name, "example.com");
|
|
assert_eq!(config.volume_name, "/opt/data");
|
|
assert_eq!(config.console_address, "192.168.1.100:9001");
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialization_deserialization_roundtrip() {
|
|
let original_config = RustFSConfig {
|
|
address: "10.0.0.1:8080".to_string(),
|
|
host: "10.0.0.1".to_string(),
|
|
port: "8080".to_string(),
|
|
access_key: "roundtrip_user".to_string(),
|
|
secret_key: "roundtrip_pass".to_string(),
|
|
domain_name: "roundtrip.test".to_string(),
|
|
volume_name: "/tmp/roundtrip".to_string(),
|
|
console_address: "10.0.0.1:8081".to_string(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&original_config).unwrap();
|
|
let deserialized_config: RustFSConfig = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(original_config, deserialized_config);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_ordering() {
|
|
let config1 = RustFSConfig {
|
|
address: "127.0.0.1:9000".to_string(),
|
|
host: "127.0.0.1".to_string(),
|
|
port: "9000".to_string(),
|
|
access_key: "admin".to_string(),
|
|
secret_key: "password".to_string(),
|
|
domain_name: "test.com".to_string(),
|
|
volume_name: "/data".to_string(),
|
|
console_address: "127.0.0.1:9001".to_string(),
|
|
};
|
|
|
|
let config2 = RustFSConfig {
|
|
address: "127.0.0.1:9000".to_string(),
|
|
host: "127.0.0.1".to_string(),
|
|
port: "9000".to_string(),
|
|
access_key: "admin".to_string(),
|
|
secret_key: "password".to_string(),
|
|
domain_name: "test.com".to_string(),
|
|
volume_name: "/data".to_string(),
|
|
console_address: "127.0.0.1:9001".to_string(),
|
|
};
|
|
|
|
let config3 = RustFSConfig {
|
|
address: "127.0.0.1:9001".to_string(), // Different port
|
|
host: "127.0.0.1".to_string(),
|
|
port: "9001".to_string(),
|
|
access_key: "admin".to_string(),
|
|
secret_key: "password".to_string(),
|
|
domain_name: "test.com".to_string(),
|
|
volume_name: "/data".to_string(),
|
|
console_address: "127.0.0.1:9002".to_string(),
|
|
};
|
|
|
|
assert_eq!(config1, config2);
|
|
assert_ne!(config1, config3);
|
|
assert!(config1 < config3); // Lexicographic ordering
|
|
}
|
|
|
|
#[test]
|
|
fn test_clone() {
|
|
let original = RustFSConfig::default_config();
|
|
let cloned = original.clone();
|
|
|
|
assert_eq!(original, cloned);
|
|
assert_eq!(original.address, cloned.address);
|
|
assert_eq!(original.access_key, cloned.access_key);
|
|
}
|
|
|
|
#[test]
|
|
fn test_debug_format() {
|
|
let config = RustFSConfig::default_config();
|
|
let debug_str = format!("{:?}", config);
|
|
|
|
assert!(debug_str.contains("RustFSConfig"));
|
|
assert!(debug_str.contains("address"));
|
|
assert!(debug_str.contains("127.0.0.1:9000"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_constants() {
|
|
assert_eq!(RustFSConfig::SERVICE_NAME, "rustfs-service");
|
|
assert_eq!(RustFSConfig::SERVICE_KEY, "rustfs_key");
|
|
assert_eq!(RustFSConfig::DEFAULT_DOMAIN_NAME_VALUE, "demo.rustfs.com");
|
|
assert_eq!(RustFSConfig::DEFAULT_ADDRESS_VALUE, "127.0.0.1:9000");
|
|
assert_eq!(RustFSConfig::DEFAULT_PORT_VALUE, "9000");
|
|
assert_eq!(RustFSConfig::DEFAULT_HOST_VALUE, "127.0.0.1");
|
|
assert_eq!(RustFSConfig::DEFAULT_ACCESS_KEY_VALUE, "rustfsadmin");
|
|
assert_eq!(RustFSConfig::DEFAULT_SECRET_KEY_VALUE, "rustfsadmin");
|
|
assert_eq!(RustFSConfig::DEFAULT_CONSOLE_ADDRESS_VALUE, "127.0.0.1:9001");
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_strings() {
|
|
let config = RustFSConfig {
|
|
address: "".to_string(),
|
|
host: "".to_string(),
|
|
port: "".to_string(),
|
|
access_key: "".to_string(),
|
|
secret_key: "".to_string(),
|
|
domain_name: "".to_string(),
|
|
volume_name: "".to_string(),
|
|
console_address: "".to_string(),
|
|
};
|
|
|
|
assert!(config.address.is_empty());
|
|
assert!(config.host.is_empty());
|
|
assert!(config.port.is_empty());
|
|
assert!(config.access_key.is_empty());
|
|
assert!(config.secret_key.is_empty());
|
|
assert!(config.domain_name.is_empty());
|
|
assert!(config.volume_name.is_empty());
|
|
assert!(config.console_address.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_very_long_strings() {
|
|
let long_string = "a".repeat(1000);
|
|
let config = RustFSConfig {
|
|
address: format!("{}:9000", long_string),
|
|
host: long_string.clone(),
|
|
port: "9000".to_string(),
|
|
access_key: long_string.clone(),
|
|
secret_key: long_string.clone(),
|
|
domain_name: format!("{}.com", long_string),
|
|
volume_name: format!("/data/{}", long_string),
|
|
console_address: format!("{}:9001", long_string),
|
|
};
|
|
|
|
assert_eq!(config.host.len(), 1000);
|
|
assert_eq!(config.access_key.len(), 1000);
|
|
assert_eq!(config.secret_key.len(), 1000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_special_characters() {
|
|
let config = RustFSConfig {
|
|
address: "127.0.0.1:9000".to_string(),
|
|
host: "127.0.0.1".to_string(),
|
|
port: "9000".to_string(),
|
|
access_key: "user@domain.com".to_string(),
|
|
secret_key: "p@ssw0rd!#$%".to_string(),
|
|
domain_name: "test-domain.example.com".to_string(),
|
|
volume_name: "/data/rust-fs/storage".to_string(),
|
|
console_address: "127.0.0.1:9001".to_string(),
|
|
};
|
|
|
|
assert!(config.access_key.contains("@"));
|
|
assert!(config.secret_key.contains("!#$%"));
|
|
assert!(config.domain_name.contains("-"));
|
|
assert!(config.volume_name.contains("/"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_unicode_strings() {
|
|
let config = RustFSConfig {
|
|
address: "127.0.0.1:9000".to_string(),
|
|
host: "127.0.0.1".to_string(),
|
|
port: "9000".to_string(),
|
|
access_key: "用户名".to_string(),
|
|
secret_key: "密码123".to_string(),
|
|
domain_name: "测试.com".to_string(),
|
|
volume_name: "/数据/存储".to_string(),
|
|
console_address: "127.0.0.1:9001".to_string(),
|
|
};
|
|
|
|
assert_eq!(config.access_key, "用户名");
|
|
assert_eq!(config.secret_key, "密码123");
|
|
assert_eq!(config.domain_name, "测试.com");
|
|
assert_eq!(config.volume_name, "/数据/存储");
|
|
}
|
|
|
|
#[test]
|
|
fn test_memory_efficiency() {
|
|
// Test that the structure doesn't use excessive memory
|
|
assert!(std::mem::size_of::<RustFSConfig>() < 1000);
|
|
}
|
|
|
|
// Note: Keyring-related tests (load, save, clear) are not included here
|
|
// because they require actual keyring access and would be integration tests
|
|
// rather than unit tests. They should be tested separately in an integration
|
|
// test environment where keyring access can be properly mocked or controlled.
|
|
}
|